Как стать автором
Обновить

Комментарии 15

Спасибо за статью. Подозреваю, что проблему рассинхронизации можно решить только если не склеивать все предложения в один документ, а поместить их также в nested поле. Рассматривали подобный вариант?

Именно так и мы и сделали, поместив предложения товара в nested поле, которое позволило делать точные запросы и агрегации по характеристикам предложений)

Но на самом деле решение проблемы рассинхронизации, это trade off между попыткой держать данные в индексе актуальными и «не убивать» кластер постоянным потоком обновлений

Очевидно, что с ростом товарного каталога и количества продавцов, а также с новыми динамическими акциями, пытаться держать индекс в актуальном состоянии по сути означает подвергать индекс постоянным обновлениям, что под капотом приводит к появлению новых сегментов в lucene индексах, что замедляет поиск (заставляя искать в большем количестве сегментов в каждом шарде), а также провоцирует эластик на периодический merge сегментов (дорогая операция в плане cpu/io).

Поэтому мы пытаемся искать золотую середину между обновлениями индекса и возможностью актуализировать цены налету

Возможно, есть смысл использовать денормализацию - например, по часто обновляемым признакам разделить документы. Получится что какие то данные дублируются, зато можно делать быстрые обновления. Сейчас при обновлении любого одного поля в документе обновляется все что в нем есть.

Но надо считать эффективность

Именно поэтому у нас два индекса:

В первом храним ровно те данные, которые необходимы для любой из возможных фильтраций/сортировок при построении индекса. Тут и цены и рейтинги в каждом коллекции для сортировок и например признак, по которому включается или включается отображение товара на сайте. Этот индекс мы хотим обновлять как можно быстрее, а данные, которые хранятся тут уже тяжело денормализовать, так как буквально каждое поле может быть задействовано в запросе на построение листинга

Во втором индексе у нас именно контентная составляющая товара: описания, картинки, атрибуты и тп. Этот индекс можно обновлять реже и вот его данные вполне можно денормализовать, но пока мы не видим в этом необходимости

А разве по атрибутам не бывает фильтрации? В своем опыте столкнулись с тем, что 80% сведений о товаре пришлось держать в главном индексе, потому что по всем ним может быть применен фильтр. Там фильтры настраиваются в админке в разрезе каждой категории и у каждого свойства есть "презумпция виновности"

Да, фильтрация происходит по атрибутам, но как вы заметили, не по всем. Поэтому можно немного задублировать информацию по атрибутам для фильтров и для их (атрибутов) отрисовки.

Во-первых, скорее всего, мы получим совершенно разные маппинги для этих задач, а, во-вторых, необходимый объем данных, который нужно хранить для фильтрации (по сути только filter_id: value) в одном индексе, будет меньше, чем тот, что нужен для отрисовки атрибута (название, описание, иконка и тп)

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

А как у вас лопаты оказывались в зоотоварах? По логике они должны быть в разных индексах.. как они смешивались?

Заголовок больше для привлечения внимания, но на самом деле, если опуститься к реализации, то все товары хранятся в одном индексе)

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

Принимая это во внимание, мы храним все товары в одном индексе, а коллекции(категории), в которые входит этот товар, храним массивом в документе товара

1) Тоесть у вас получается по 1й схеме из эластика может вернутся 100000 ids подходящих под запрос , но при этом эластик не содержит цены , если пользователь указал фильтр цены , получается вы на стороне сервеса будете через базу отсеивать резульаты? select * where Price > 10 and IDS IN (....10000000 return from ES) . Этоже абсолютно не хайлоуд?!

Как при этом происходит пагинация? я даже не представляю

2) Как я понял у вас в ES отдельно только важное о товарах, при этом есть сортировка по лайкам которые очевидно в другой базе данных, как происходит трансфер данных ? Опять из эластика приходит миллион подходящих результатов , а потом через базу с лайками сортируете ? Так же хочу обратить внимание почему я пишу миллионы , т.к. если у вас товары в 1 базе , а сортировка в другой , то нельзя вернуть "первые 100 подходящие под запрос ES", т.к. по сортировки лайков это 100 вернувшихся вообще могут быть далеко не первые .

В общем интересны эти моменты

Как я понял у вас в ES отдельно только важное о товарах

Именно так, и под «важным» мы подразумеваем именно те данные, которые могут влиять на выдачу. То есть все параметры, которые участвуют в фильтрах или сортировках, присутствуют в индексе)

Если я правильно понял, то в первом вопросе речь идёт именно про текстовый поиск товаров, когда большой набор идентификаторов приходит к нам из другого сервиса? При этом верно, что тот «другой» сервис не занимается фильтрацией по цене, но этим занимаемся мы в своём индексе, примерно по той же схеме, что и была описана в вопросе) Пагинацию в такой схеме также делаем мы на своей стороне, что обеспечивает ее работоспособность. Схема имеет много недостатков и прислать в таком виде миллион товаров, конечно будет затруднительно, поэтому мы ограничиваемся более маленькими выдачами.

Что же касается не текстовых запросов, а например категорийных, то все данные для такого запроса есть в нашем индексе и отфильтровать по цене мы можем уже в нем. Далее мы просто налету актуализируем цены товаров, которые мы нашли по эластику для первой страницы, и, возможно, видоизменяем выдачу при отставании индекса

>> на лету актуализируем цены товаров
Значит ли это, что теоретически при сортировке по цене порядок может сбиться на стадии актуализации?

Если я правильно понял, то описанное ES окружение используется в качестве хранилища и на него не приходят сложные полнотекстовые запросами и нет сложных аналайзеров на полях. А если это так, то зачем тут ES? Его можно заменить на более производительный способ хранения, например на mongodb. Или есть причины использовать тут ES?

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

Почему же тогда ES? Ответ достаточно прост: в нашем ландшафте нет монги, как следствие нет и боевого опыта ее использования как у разработки, так и у девопсов (да он есть у некоторых людей в отдельности, но массового понимания как ее готовить - нет). Поэтому мы просто не стали брать на себя роль первопроходцев и решили остановиться на более проверенном для нас решении

ElasticSearch разделяет query и filter контексты запроса. ... 

это все размещается в блоке query, наверно имелось ввиду различия меж filter и must.

  • filter - простоя отсев по критериям без привлечения механизма расчета релевантности (score);

  • must - все тоже самое, но +расчет релевантности / подключение анализаторов нечеткого поиска

Я бы порекомендовал еще dynamic: strict в настройках индекса, но это уже вопрос вкусовщины

Для того чтобы решить эту проблему, мы снова воспользовались nested-типом поля....

если память не изменяет, то nested поля представляются как вложенные документы, связанные с корневым документом и при поисковом проходе происходит динамическое обращение по ссылке (для простоты назовем так) к вложенному документы для сканирования. Да, на первый взгляд, так удобно, но добавляется оверхед.

Плюшевая игрушка - слоник с синим телом и серым хоботом, тогда запрос будет:

"query": {
    "nested": {
    "path": "nested_filters",
    "query": {
        "bool": {
            "filter": [
               {
                    "nested": {
                        "path": "nested_filters",
                        "query": {
                            "bool": {
                                "filter": [
                                    {
                                        "term": {
                                            "nested_filters.filter_id": "туловище"
                                        }
                                    },
                                    {
                                        "term": {
                                            "nested_filters.string_values": "синий"
                                        }
                                    }
                                ]
                            }
                        }
                    }
                },
                {
                    "nested": {
                        "path": "nested_filters",
                        "query": {
                            "bool": {
                                "filter": [
                                    {
                                        "term": {
                                            "nested_filters.filter_id": "хобот"
                                        }
                                    },
                                    {
                                        "term": {
                                            "nested_filters.string_values": "серый"
                                        }
                                    }
                                ]
                            }
                        }
                    }
                }
            ]
        }
    }
}

и в этом сценарии будет просадка по производительности, ибо будет 2 обращение по ссылке и 2 сканирования всех вложенных документов

все товары храняться под 1 элиасом, а что если шардировать по региону? Да, кол-во алиасов вырастет, но до конечного числа (равное кол-ву регионов). Если проанализировать распределение по регионам, то станет ясно что кол-во документов по каждому региону разниться. Т.е. применение фильтра по региону без примение фильтра по региону и сразу стрелять в региональный элиас.
Таким образом можно избавиться от мега-тяжелого алиаса "уаще всех товаров" в пользу мелких регионально-таргетированых, снизив/разделив нагрузку по регионам и разделив точку отказа по регионам

P.s. на самом деле оптимизация на этом не заканчивается, я бы сфокусировался на оптимизации хранения. С оптимизацией хранения значительно вырастит производительность и упадет нагрузка на I/O, CPU

это все размещается в блоке query, наверно имелось ввиду различия меж filter и must.

Тут есть трудности с неймингом этих контекстов - пересечения названий query и filter как блоков запроса и одновременно как контекстов выполнения всего запроса. На самом деле на filter и must разделение не заканчивается, также есть например should/must_not, которые также выполняются в разных контекстах) Подробнее об этом можно посмотреть тут https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#query-context

Я бы порекомендовал еще dynamic: strict в настройках индекса, но это уже вопрос вкусовщины

Согласен, для таких задач, хорошая практика использовать strict мапинг, но так как сам индекс создаём мы же, вероятность добавления новых полей без нашего участия исключена

Да, на первый взгляд, так удобно, но добавляется оверхед.

Плюшевая игрушка - слоник с синим телом и серым хоботом, тогда запрос будет:

Дело чуть больше, чем просто в удобстве: без нестед типа маппинг был расширялся до бесконечности, так как таких filter_id с ростом товаров появляется все больше и больше и невозможно делать их частью маппинга.

Однако, в статье это было упущено, но хранение фильтров мы все-таки немного переосмыслили. Так точные фильтры (которые работают по принципу равенства, без запросов больше/меньше) мы поместили во flattened тип поля (https://www.elastic.co/guide/en/elasticsearch/reference/current/flattened.html) . Он позволяет не использовать nested связи и строить запросы так, будто мы обращаемся к полю в мапинге, при этом не раздувая его:

{
  "query": {
    "bool": {
      "filter": [
        {
           "term":{
             "flattened_filters.хобот": "серый"
           }     
        },
        {
          "term": {
            "flattened_filters.туловище": "синий"
          }
        }
      ]
    }
  }
}

Однако есть ограничения на типы запросов, которые можно делать к таким полям. Поэтому фильтры, по которым делаются запросы больше/меньше, мы храним по той же схеме, что была описана в статье)

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

все товары храняться под 1 элиасом, а что если шардировать по региону? Да, кол-во алиасов вырастет, но до конечного числа (равное кол-ву регионов). Если проанализировать распределение по регионам, то станет ясно что кол-во документов по каждому региону разниться

В целом - да, шардировать по региону это хорошая идея и мы держим ее в голове на случай, если мы упремся в производительность эластика)

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий