Elasticsearch — как работает система полнотекстового поиска: плюсы и минусы, альтернативы и лайфхаки

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

В этой статье расскажем, что такое Elasticsearch и как им пользоваться, рассмотрим его преимущества и недостатки, покажем примеры использования. А также поделимся лайфхаками использования этого инструмента для поиска. 


ЧТО ТАКОЕ ELASTICSEARCH

Elasticsearch — это система полнотекстового поиска, написанная на Java. Кроме того, Elasticsearch — это нереляционное хранилище документов в формате JSON, которое выпущено как проект с открытым исходным кодом в соответствии с условиями лицензии Apache.

КАК РАБОТАЕТ

 

В Elasticsearch отправляются данные в виде документов JSON с помощью API или такого инструмента, как Logstash. Elasticsearch сохраняет документ в индекс кластера и делает его доступным для поиска. После этого можно найти и извлечь документ, используя API Elasticsearch. 

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

ПОЧЕМУ УДОБНО РАБОТАТЬ С ELASTICSEARCH

 

В сервисах с большим количеством данных, а именно e-commerce проектах, интернет-магазинах, онлайн-аптеках, маркетплейсах, сайтах знакомств, поиск — это фасеты. Фасеты — это условия для поиска, например, набор тегов или фильтр по значениям. 

 

Работая с большим объемом данных,  мы сталкиваемся со стандартной схемой данных для типичного e-commerce в СУБД, и выглядит она примерно так:

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

АЛЬТЕРНАТИВЫ ELASTICSEARCH

Перед тем, как начать работать с Elasticsearch, рассмотрим альтернативные СУБД, которые могут помочь сделать поиск удобным — MongoDB и PostgreSQL. 

MongoDB:

  • Нет схем, то есть не нужно заморачиваться о структуре данных, вносим всё, что угодно;
  • Web scale — при повышении нагрузки масштабирование делается очень легко как по горизонтали, так и по вертикали;
  • Отсутствует аналог конструкции JOIN, а это значит, что мы не тратим время на соединение таблиц, в документе уже все есть;
  • Вся валидация делается в коде. Если вы откроете любое руководство, вы увидите, что сначала ставят драйвер для MongoDB, а только потом библиотеку валидаций;
  • Нет аналогов хранимых процедур. Опять же — все в коде.

PostgreSQL:

  • Есть нереляционное хранилище в формате JSON, можно назначать ограничения на поля. Например, есть таблица пользователя, у пользователя есть персональные данные, надо ввести на них ограничения. JSON отлично справится с этим;
  • Наличие TextSearch функций, которые обеспечивают работу полнотекстового поиска;
  • Есть хранимые процедуры;
  • Большое количество таблиц/столбцов/индексов;
  • Не все ORM понимают JSON.  

Используя MongoDB и PostgreSQL для полнотекстового поиска, мы получим ограниченную функциональность. В некоторых случаях её будет достаточно, но для полноценного удобства предлагаем использовать Elasticsearch (далее — ES). У него также есть свои плюсы и минусы.

ПРЕИМУЩЕСТВА

  1. Для общения с Elasticsearch используется RESTful API или, если говорить простым языком, обыкновенные HTTP-запросы. И работать с ним можно прямо из браузера. Для просмотра информации о сервисе достаточно обратиться по адресу localhost:9200;
  2. Можно сделать валидацию на уровне хранилища, в ES это называется маппингом (mappings);
  3. ES умеет работать с разными запросами — простыми, сложными, структурированными — и различными типами данных;
  4. JSON QUERY — ещё более декларативный синтаксис, чем SQL;
  5. Kibana — визуальный инструмент для ES, чтобы взаимодействовать с данными, которые хранятся в индексах ES. Веб-интерфейс Kibana позволяет выполнять и тестировать запросы, настраивать сам ES-кластер и многие другие вещи. 

 

Кроме преимуществ, у ES есть, конечно, и недостатки. 

НЕДОСТАТКИ

Разработчики ES предоставляют нужные библиотеки для большинства популярных языков в разделе Elasticsearch Clients. И по ряду причин это не самый лучший выбор. Клиент для каждого языка имеет свои несовершенства:

  • Java Client — вложенные цепные вызовы на лямбдах, синтаксис на любителя;
  • JavaScript Client — максимально похож на jQuery.ajax — в целом работает и ладно;
  • Go Client — можно вызвать Search().Raw([]byte), т.е. мы точно так же пишем многострочный текстовый запрос, как мы это делали в примере с PHP и HEREDOC;
  • .NET Clients — что-то среднее между Java- и Go-клиентами;
  • PHP Client — не умеет работать с шаблонами и в целом мало что умеет;
  • Python Client — предлагает очень странный DSL.

Используя Elasticsearch Clients, мы пытаемся взять запрос в самом ES в формате JSON, например:

 

{

 "_source" : [

   "guidEsId",

   ... еще 36 полей ...

   "expireDates.77"

 ],

 "query" : {

   "function_score" : {

     "query" : {

       "bool" : {

         "must" : [

           {"exists" : {"field" : "priceBuyer.77"}},

           {"terms" : {"mnn" : ["пурген"]}},

           {"term" : {"active" : "1"}},

           {"regexp" : {"itemName" : ".*таблетки.*25мг.*"}},

           {"range" : {"packQuantity" : {"lte" : 10}}},

           {"range" : {"priceBuyer.77" : {"gt" : 0}}},

           {"terms" : {"itemGroup" : [96]}},

           {

             "bool" : {

               "should" : [

                 {"range" : {"quantities.77.1" : {"gt" : 0}}},

                 {"range" : {"quantities.77.8" : {"gt" : 0}}},

                 {"range" : {"quantities.77.7" : {"gt" : 0}}},

                 {"range" : {"quantities.77.6" : {"gt" : 0}}}

               ]

             }

           }

         ]

       }

     }

   }

 }

}

 

 

и построить его в привычном ООП-стиле. Но на выходе в ES Clients получается такой результат:

 

SearchResponse<Product> search = new ElasticsearchClient().search(s -> s

   .query(q -> q

       .bool(b -> b

           .must(m -> m

               .exists(...)

               .terms(...)

               .term(...)

               .regexp(...)

               .range(...)

               .range(...)

               .terms(...)

               .bool(b -> b

                   .should(sh -> sh

                       .range(...)

                       .range(...)

                   )

               )

           )

       )

   )

)

 

 

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

 

Поэтому предлагаем альтернативу Elasticsearch Clients: 

Клиент для языка программирования Чем предлагаем заменить
Java Client org.apache.http.client.HttpClient
JavaScript Client node:http/axios/Fetch API
Ruby Client Net::HTTP
Go Client net/http
.NET Clients System.Net.Http.HttpClient
PHP Client curl/GuzzleHttp
Python Client requests или httpx
Python Client reqwest

ЛАЙФХАКИ, КОТОРЫЕ ПОМОГУТ ВАМ ПРИ ИСПОЛЬЗОВАНИИ ELASTICSEARCH

  • Вам не всегда нужен ESClient

Несмотря на то, что мы говорили раньше, в начале пути можно и нужно пользоваться ESClient. Поскольку сначала писать на JSON-языке запросов может быть непривычно и крайне сложно. Со временем у вас соберется пул типичных запросов, которые выполняются всегда. В этот момент вы уже, скорее всего, устанете писать на цепных запросах в Query Builder. Тогда и можно будет перейти к стадии «выкидываем ESClient» и изучить шаблоны, в частности шаблонизатор Mustache.

  • Опасайтесь вложенных полей

У ES есть «вложенные поля» — nested object type. Содержимое полей хранится как отдельный документ и может сэкономить память. Например, если у вас есть список пользователей, и есть внутренний объект «организация, к которой принадлежит этот пользователь». Если вы сделаете индекс привычным образом, у каждого пользователя будет, условно говоря, «своя уникальная организация» как отдельное поле. Если сделать nested-объекты, в nested-индексе сохранится один объект организации, а все остальные пользователи будут просто на него ссылаться. Мы по факту сжимаем и входные, и индексированные данные.

 

  • Сжимайте ответы 

В ES есть &filter_path. Это параметр, который позволяет указать, какие поля из итогового ответа вы забираете. Потому что в ES в поиске возвращает объект, в котором есть поле hits, в котором лежит поле hits, который является массивом, в котором лежат объекты. И внутри этого объекта, например, поля _source и _score.

  • hits.hits._source — вернет тело документа;
  • hits.hits._score — вернет рейтинг соответствия;
  • hits.hits._id — вернет внутренний ID документа.

Кроме этого, ES возвращает много дополнительных данных, а filter_path позволяет обрезать ответ.

  • Фильтруйте то, что уже отфильтровано 

Вы часто видели в интернет-магазинах возможность выбрать телефон на 16ГБ оперативной памяти, 32ГБ и 64ГБ. Можно поставить галочку на 32ГБ, и все остальные варианты скрываются как неактивные. В этом случае работает фильтр, и другие варианты фильтрации отображаться не будут. Но иногда пользователю нужно оставить и другие опции доступными для выбора. Для этого в ES есть post_filter — он фильтрует то, что уже отфильтровано и сгруппировано. В конечном итоге  у пользователя есть отфильтрованный список тех параметров, которые ему нужны в телефоне. Но при этом остаются доступными и другие опции, по которым также можно искать. 

 

{

     "query": {

         "bool": {

            "filter": {

              "term": {"brand": "gucci" }

            }

         }

     },

     "aggs": {

        "colors": {

           "terms": { "field": "color" }

        },

        "color_red": {

           "filter": {

             "term": { "color": "red" }

           },

           "aggs": {

              "models": {

                   "terms": { "field": "model" }

               }

           }

    }

 },

"post_filter": {

   "term": { "color": "red" }

  }

}

 

В этом примере мы показываем пользователю, какие еще цвета доступны для поиска, но показываем только красные модели.

  • Пользуйтесь Aliases

Alias — это псевдонимы индексов. Изначально в чем проблема — чаще всего мы смотрим в один индекс. И у этого индекса статическое имя, которое где-то в настройках уже лежит. Иногда, чтобы добавить данные, нужно перестроить индекс. Но заставлять пользователя ждать индексацию — не разумно, это занимает много времени. В ES можно сделать это в фоновом режиме. Мы просто создаем новый индекс и наполняем его. В это время пользователь спокойно ищет данные по старому индексу и не знает, что данные о товаре поменялись. После этого мы просто переключаем индекс, это занимает буквально 10 миллисекунд. Пользователь даже ничего не заметит. 

{
	"actions" : [
	      { "remove" : { "index" : "НАШ_ИНДЕКС_2000_01_01", "alias" : "ИМЯ ИНДЕКСА" } },
                  { "add" : { "index" : "НАШ_ИНДЕКС_2022_09_17", "alias" : "ИМЯ ИНДЕКСА" } }
	]
}

 

 

  • Не индексируйте то, что не ищете

Например, есть организация, в организации есть люди, у людей есть машины. Кроме этого, в данных организации у человека указан опыт вождения в годах. Можно искать человека по любым данным — по имени, фамилии, можно искать по личному номеру автомобиля. Но вряд ли кто-то будет искать человека по опыту вождения. Соответственно, индексируйте только то, что ищете. Просто указывайте тип и отключите индексацию поля через {"index": false}:  

{ "mappings": { "properties": { "exp": { "type": "integer", "index": false }}}}
  • Читайте How-to

How-to — это гайд по оптимизации работы с ES от его разработчиков. Читая его, вы лучше поймете, что и как использовать, а также сможете выполнить ряд оптимизаций для повышения производительности вашего проекта.

В ИТОГЕ

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

ПОЛЕЗНЫЕ ССЫЛКИ