ElasticSearch 1.0 — новые возможности аналитики

от автора

Многие слышали о высокоуровневом поисковом сервере ElasticSearch, но не все знают. что многие используют его не совсем по прямому назначению. Речь идет о реалтайм-аналитике различных структурированных и не очень данных.

Эта статья также назрела ввиду того, что многие крупные интернет-проекты рунета в 2014 году получили письма счастья от Google Analytics с предложением заплатить $150 000 за возможность использовать их продукт. Я лично считаю, что ничего плохого в том, чтобы оплатить труд программистов и администраторов нет. Но при этом это довольно серьезные инвестиции, и, может, вложения в собственную инфраструктуру и специалистов, даст большую гибкость в дальнейшем.

Аналитика в ElasticSearch основана на полнотекстовом поиске и фасетах. Фасеты в поиске — это некая агрегация по определенному признаку. Вы часто сталкивались с фасетами-фильтрами в интернет-магазинах: в левой или правой колонке есть уточняющие галочки. Ниже пример тестового фасетного поиска у нас на главной странице http://indexisto.com/.

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

Так как тема еще не освещалась на Хабре, я хочу рассказать, что из себя представляют аггрегации в ElasticSearch, какие возможности открываются и есть ли жизнь без Hadoop.

Во-первых, я хотел бы привести статистику из Google Trends, которая наглядно показывает, насколько велик интерес к ElasticSearch:

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

Распределенные вычисления

Запрос исполняется на кластере параллельно, на тех шардах, в кототорых лежит интересующий индекс. Шарды — это, по сути, старые добрые Lucene-индексы. Все, что делает ElasticSearch, это принимает запрос, определяет ноды, где лежат шарды индекса, к которому пришел запрос, рассылает запрос на эти шарды, шарды считают и отсылают результаты ноде-инициатору. Нода-инициатор уже делает свертку и отсылает клиенту. По потокам данных это похоже на map reduce, когда индексы Lucene — это mapper’ы, а нода-инициатор — это reducer.

Некоторые запросы могут выполниться в один проход, некоторые в несколько. Например, чтобы посчитать стандартную текстовую релевантность tf/idf, основанную на частоте терма в документе (tf) и в коллекции документов (idf), очевидно, надо сначала получить idf с учетом всех шардов (первый запрос к шардам), и потом уже сделать основной запрос (второй). Хотя для скорости можно idf посчитать и локально на шарде.

Сделаем фасетный запрос «Сколько же было ошибок в логах по годам». Этот запрос ищет ERROR в документах логов, и делает фасеты по годам.
Вот так, примерно, выглядят потоки данных:

Нода, к которой пришел запрос, исполнила его сама (так как на ней есть один из шардов нужного индекса) и разослала на другие ноды, где содержатся другие шарды индекса. Каждый шард исполняет примерно такую последовательность в индексе:

  • определить максимальное и минимальное значение в поле date. Lucene не хранит внутри даты или числа — все в текстовом представлении.
  • подготовить нужные диапазоны дат в соответствии с тем, какое разбиение нужно пользователю. У нас это 1 год. Если логи есть за 2 года то получится 2 запроса
  • пройтись по инвертированному индексу и посчитать все документы (записи в лог), которые содержат ERROR и попадают в текущий диапазон. Сделать такой запрос для всех диапазонов.

Небольшая справка по Lucene. Range запросы по «числам» в текстовом представлении очень эффективны NumericRangeQuery:

Comparisons of the different types of RangeQueries on an index with about 500,000 docs showed that TermRangeQuery in boolean rewrite mode (with raised BooleanQuery clause count) took about 30-40 secs to complete, TermRangeQuery in constant score filter rewrite mode took 5 secs and executing this class took <100ms to complete 

Переходим к практике. Подготовим данные

Так как сейчас мы просто посмотрим, что можно нааггрегировать в ElasticSearch, то не будем гнаться за объемами, а заведем индекс habr и добавим в него 20 постов.
Посты будут вот такой структуры:

{         "user": "andrey",         "title": "Android заголовок 1",         "body": "Android тело 1",         "postDate": "2011-02-15T11:12:12",         "tags": ["взрыв"],         "rank": 67,         "comments": 21 } 

Я создал 10 постов про Android и 10 постов про iPhone.
Посты могут иметь один или несколько тегов из набора

  • взрыв
  • обновление
  • приложение
  • баг

Годы постов от 2011 до 2014. Рейтинг от 0 до 100. Кол-во комментариев аналогично.

Вот что у нас в индексе в итоге:

Old-school-анализ (до версии ES 1.0)

Распределение тегов в постах про Android
Сразу на практике покажем, что можно проанализировать, сочетая полнотекстовый поиск и фасеты. Делаем поисковый запрос android к полю Title и фасетный запрос по полю tags:

{   "query": {     "wildcard": {       "title": "Android*"     }   },   "facets": {     "tags": {       "terms": {         "field": "tags"       }     }   } } 

В ответ получаем:

"facets": {   "tags": {     "_type": "terms",     "missing": 0,     "total": 18,     "other": 0,     "terms": [       {         "term": "обновление",         "count": 5       },       {         "term": "баг",         "count": 5       },       {         "term": "приложение",         "count": 4       },       {         "term": "взрыв",         "count": 4       }     ]   } } 

Ну и для сравнения то же самое, но если мы ищем посты про iPhone:

"facets": {   "tags": {     "_type": "terms",     "missing": 0,     "total": 16,     "other": 0,     "terms": [       {         "term": "взрыв",         "count": 5       },       {         "term": "баг",         "count": 5       },       {         "term": "приложение",         "count": 4       },       {         "term": "обновление",         "count": 2       }     ]   } } 

Как видно из аналитики, айфоны чаще взрываются, а андроиды обновляются.

Гистограмма распределения постов про iPhone по годам
Еще один пример, на котором мы рассмотрим, как менялся интерес к айфонам со временем. Запрос iPhone, фасеты по полю postDate:

{   "query": {     "wildcard": {       "title": "iPhone"     }   },   "facets": {     "articles_over_time": {       "date_histogram": {         "field": "postDate",         "interval": "year"       }     }   } } 

Ответ:

"facets": {   "articles_over_time": {     "_type": "date_histogram",     "entries": [       {         "time": 1293840000000,         "count": 2       },       {         "time": 1325376000000,         "count": 4       },       {         "time": 1356998400000,         "count": 2       },       {         "time": 1388534400000,         "count": 2       }     ]   } } 

Как видно, всплеск интереса пришелся на 2012 год (в ответе timestamp’ы).

Примерно понятно, как это работает. Основные типы фасетов в ElasticSearch

  • term — удобно на полях типа tags. Не очень рекомендуется строить term-фасет по терминам из «Войны и мира»
  • range — фасеты по диапазону значений числовых полей и дате. Можно сделать фасет: посчитай мне все товары с ценой от 0 до 100 и от 100 до 200
  • histogram — это те же самые range-запросы, только диапазоны задаются автоматически, указывается шаг разбиения. Работает по числовым полям и дате. Не стоит делать гистограмму с шагом в 1 миллисекунду за 100 лет

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

Аналитика в ES 1.0 — new school!

Как вы заметили, фасеты в ES до версии 1.0 просто возвращали число документов. Запросы были довольно плоскими (никакой вложенности). Но что если мы хотим получить посты про iPhone по годам, а потом посчитать средний рейтинг этих постов?
В ES 1.0 это стало возможно благодаря Aggregation-фреймворку.

Существуют два вида аггергаций:
Bucket — которые считают число найденных документов, а также складывают в «корзину» id найденных документов. Например, приведенные выше фасетные запросы типа term, range, histogram — это Bucket-агрегаты. Теперь они не просто считают количество документов, например, по термам в поле тегов, но и выдают списки id этих документов: 7 документов с тегом «баг», id этих документов 2,5,6,10,17,19,20
Calc — которые считают только число. Например, среднее значение числового поля, минимумы, максимумы, суммы.

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

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

  • Ограничиваем выборку постами, которые содержат iPhone в заголовке
  • делаем histogram-bucklet-фасеты по годам
  • по полученным «корзинам» делаем term-bucklet-фасеты по тегам.
  • по полученным «корзинам» (с тегами) делаем Average-агрегацию по рейтингу
  • Так как нам нужны только самые скучные теги, то отсортируем их по значению, полученному во вложенной агрегации (средний рейтинг).
{   "query": {     "wildcard": {       "title": "iphone"     }   },   "aggs": {     "post_over_time_agg": {       "date_histogram": {         "field": "postDate",         "interval": "year"       },       "aggs": {         "tags_agg": {           "terms": {             "field": "tags",             "size": 1,             "order": {               "avg_rating_agg": "asc"             }           },           "aggs": {             "avg_rating_agg": {               "avg": {                 "field": "rank"               }             }           }         }       }     }   } } 

Ответ:

"aggregations": {   "post_over_time_agg": {     "buckets": [       {         "key": 1293840000000,         "doc_count": 2,         "tags_agg": {           "buckets": [             {               "key": "взрыв",               "doc_count": 1,               "avg_rating_agg": {                 "value": 14.0               }             }           ]         }       },       {         "key": 1325376000000,         "doc_count": 4,         "tags_agg": {           "buckets": [             {               "key": "взрыв",               "doc_count": 1,               "avg_rating_agg": {                 "value": 34.0               }             }           ]         }       },       {         "key": 1356998400000,         "doc_count": 2,         "tags_agg": {           "buckets": [             {               "key": "приложение",               "doc_count": 2,               "avg_rating_agg": {                 "value": 41.0               }             }           ]         }       },       {         "key": 1388534400000,         "doc_count": 2,         "tags_agg": {           "buckets": [             {               "key": "баг",               "doc_count": 1,               "avg_rating_agg": {                 "value": 23.0               }             }           ]         }       }     ]   } } 

Как видно, в 2011 и 2012 году людей достали посты про взрывы айфонов, в 2013 про новые приложения, а в 2014 про баги.

К реальным задачам

Стандартная задача интернет-магазина — посчитать эффективность рекламной кампании или проанализировать поведение группы пользователей. Представьте, что вы пушите все логи вашего http-сервера в ElasticSearch, при этом добавляя в документ еще немного своих данных, которые у вас хранятся, например в Битриксе

{   "url": "http://myshop.com/purchase/confirmed",   "date": "2014-02-15T11:12:12",   "agent": "Chrome/32.0.1667.0",   "geo": "Москва",   "utm_source": "super_banner",   "userRegisterDate": "2011-02-15T11:12:12",   "orderPrice": 4000,   "orderCategory": ["Сотовые телефоны","айфоны"] } 

При этом схема документа не фиксирована. Есть на этой странице заказ — добавили orderPrice, нет — не добавили.
Имея всего лишь такие логи, вы уже можете многое из того, что умеет Google Anallytics.

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

  • делаем текстовый запрос по url слова «purchase/confirmed»
  • делаем date_histogram-фасеты по дате регистрации пользователя
  • делаем date_range-фасет с начальным значением диапазона «дата регистрации пользователя» (к ней можно обратиться в запросе) и конечным + 2 месяца
  • делаем count и average по полю orderPrice в полученных bucklet

Пример 2: Отcлеживать цели и конверсии по каналам с разбивкой по дате. Например, цель — посещение урлы /purchase/confirmed
Запрос:

  • делаем текстовый запрос по url слова «purchase/confirmed»
  • делаем date_histogram-фасет по дате посещения урлы с разбивкой в 1 день
  • делаем term-фасет по utm_source
  • делаем count-агрегацию

Выводы

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

Еще добавлю, что порог вхождения очень низкий. Установка за 5 минут, дальше можно играться хоть с curl, хоть плагином к Chrome который умеет генерировать http-запросы. У нас http://indexisto.com/ сейчас кластер из 7 машин показывает себя на удивление стабильно.

Все возможности аггрегаций описаны здесь https://github.com/elasticsearch/elasticsearch/issues/3300

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/213849/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *