Elasticsearch — отличное хранилище данных для реализации полнотекстового поиска и аналитики. Если ваш вариант использования требует этого, я настоятельно рекомендую использовать его. Тем не менее, есть определенные приложения, в которых вам нужно быть осторожным при индексации ваших данных.

Пользовательский поток данных

Предположим, вы крупная медицинская страховая компания с миллионами клиентов, и по каждому клиенту у вас есть множество данных о пациентах. Теперь предположим, что вы хотите иметь возможность эффективно искать данные пациентов и решили использовать Elasticsearch. Как бы вы это сделали?

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

Наличие индекса на пациента работает, если количество пациентов не слишком велико. Но у нашей страховой компании миллионы пациентов, а значит, у нее миллионы индексов Elasticsearch. В этом случае Elasticsearch замедлится до минимума.

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

Несколько примеров с кодом

В следующих примерах кода я использую Ruby 2.3.3 и Elasticsearch версии 2.4.2. В Ruby есть отличная жемчужина Elasticsearch, которая позволяет легко продемонстрировать, что происходит. Документация для этого драгоценного камня также очень хороша. Я использую Elasticsearch локально на порту 9200.

Давайте потребуем гем Elasticsearch и библиотеку Ruby JSON и убедимся, что наш экземпляр Elasticsearch запущен и работает.

Запуск client.info дает нам следующий результат:

pry(main)> client.info
=> {"name"=>"Dredmund Druid",
 "cluster_name"=>"elasticsearch_mkim",
 "cluster_uuid"=>"f19ZvZWLTsaJeUM9ww1nQw",
 "version"=>
  {"number"=>"2.4.2",
   "build_hash"=>"161c65a337d4b422ac0c805f284565cf2014bb84",
   "build_timestamp"=>"2016-11-17T11:51:03Z",
   "build_snapshot"=>false,
   "lucene_version"=>"5.5.2"},
 "tagline"=>"You Know, for Search"}

Теперь нам нужно создать первичный индекс, на который будут ссылаться все псевдонимы. Я называю этот индекс patients. Я не смог найти удобную команду в геме Elasticsearch для выполнения базового HTTP-запроса PUT, поэтому для этих типов запросов я прибегаю к использованию Faraday. (Если кто-то знает о команде, пожалуйста, не стесняйтесь писать в комментариях.)

Теперь давайте создадим псевдонимы для двух пациентов с именами PatientA и PatientB.

Как видите, отображение псевдонима выполняется через поле patient_name. Для тех, кто все еще следит за вами, возможно, вы заметили, что в теле запроса есть дополнительное поле routing. Это поле важно, и далее я объясню, почему.

Как правило, данные пациентов хранятся в нескольких разных сегментах. Когда поиск выполняется в данных конкретного пациента, Elasticsearch необходимо выполнить поиск в нескольких разных сегментах, а затем объединить эти результаты. Это неэффективно. Лучшим способом было бы хранить все данные по конкретному пациенту в одном шарде. Elasticsearch позволяет нам делать это через маршруты. Предоставляя конкретный маршрут, Elasticsearch будет знать, что нужно хранить данные конкретного пациента в сегменте, указанном маршрутом. Вот для чего предназначено поле routing.

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

Теперь мы можем выполнить поиск!

pry(main)> client.search index: 'patients', q: 'content:The quick brown'
=> {"took"=>4,
 "timed_out"=>false,
 "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0},
 "hits"=>
  {"total"=>1,
   "max_score"=>0.26442188,
   "hits"=>
    [{"_index"=>"patients",
      "_type"=>"patient_type",
      "_id"=>"AVkBDPk-1ECoVlT3taI2",
      "_score"=>0.26442188,
      "_routing"=>"PatientA",
      "_source"=>{"patient_name"=>"PatientA", "content"=>"The quick brown fox"}}]}}
pry(main)> client.search index: 'PatientA', q: 'content:The quick brown'
=> {"took"=>2,
 "timed_out"=>false,
 "_shards"=>{"total"=>1, "successful"=>1, "failed"=>0},
 "hits"=>
  {"total"=>1,
   "max_score"=>0.26442188,
   "hits"=>
    [{"_index"=>"patients",
      "_type"=>"patient_type",
      "_id"=>"AVkBDPk-1ECoVlT3taI2",
      "_score"=>0.26442188,
      "_routing"=>"PatientA",
      "_source"=>{"patient_name"=>"PatientA", "content"=>"The quick brown fox"}}]}}

Обратите внимание: когда мы выполняли поиск по первичному индексу patients, мы видим, что в поиске участвовали все пять осколков. При поиске по псевдониму PatientA использовался только один осколок. Похоже, псевдоним работает. Также похоже, что маршрутизация работает, поскольку при поиске по псевдониму использовался только один шард.

Вывод

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