ElasticSearch — агрегация данных
В статье мы рассмотрим, как правильно реализовывать агрегацию данных, зачем это может понадобиться, и сдобрим это кучей рабочих примеров.Для всех, кому интересно как сделать свои запросы в ES интереснее и посмотреть на обычной поиск с другой стороны, прошу под кат.
В предыдущей статье пользователи разделились поровну между статьёй по более простой теме и по более сложной, поэтому я выбрал не очень сложную тему, но довольно свежую, которая добавилась в ES относительно недавно (v1.0) и несёт довольно интересный функционал.
Aggregation moduleЭтот модуль пришел в ES на смену Facets, причем в настойчивой форме, Facets теперь считаются устаревшими и будут удалены в ближайшие релизы. Хотя агрегаты и были добавлены в v1.0.0RC1, а сейчас уже >1.2, я все же не рекомендую использовать Facets.Зачем же понадобилось изменять рабочий инструмент? Наверное, главной фишкой агрегатов является их вложенность. Приведу общий синтаксис запроса:
«aggregations» : {
»
Типы агрегатов Типов агрегатов очень много, но все их можно объединить в 2 главных типа: — Bucketing (Обобщение)Для простоты понимания, это можно сравнить со всем знакомым инструментов «GROUP BY». Конечно, это довольно упрощенное сравнение, но принцип работы схож. Этот тип на основе фильтров обобщает документы, по какому-то определённому признаку, хороший пример это terms aggregation.
— Metric (Метрические)Это агрегаты, которые высчитывают какие либо значение по определенному набору документов. Например sum aggregation
Думаю, для начало теории хватит, всем, кого интересует более фундаментальная информация по этому модулю, могут ознакомится с ней по этой ссылке.
Простой пример Желающим попробовать всё собственноручно предлагаю использовать этот дампСтруктура и данные для теста Дамп наглым образом взят из этой прекрасной статьи curl -XPUT «http://localhost:9200/sports/» -d' { «mappings»: { «athlete»: { «properties»: { «birthdate»: { «type»: «date», «format»: «dateOptionalTime» }, «location»: { «type»: «geo_point» }, «name»: { «type»: «string» }, «rating»: { «type»: «integer» }, «sport»: { «type»: «string» } } } } }'
curl -XPOST «http://localhost:9200/sports/_bulk» -d' {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Michael», «birthdate»:»1989–10–1», «sport»: «Baseball», «rating»: [»5»,»4»], «location»:»46.22,-68.45»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Bob», «birthdate»:»1989–11–2», «sport»: «Baseball», «rating»: [»3»,»4»], «location»:»45.21,-68.35»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Jim», «birthdate»:»1988–10–3», «sport»: «Baseball», «rating»: [»3»,»2»], «location»:»45.16,-63.58» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Joe», «birthdate»:»1992–5–20», «sport»: «Baseball», «rating»: [»4»,»3»], «location»:»45.22,-68.53»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Tim», «birthdate»:»1992–2–28», «sport»: «Baseball», «rating»: [»3»,»3»], «location»:»46.22,-68.85»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Alfred», «birthdate»:»1990–9–9», «sport»: «Baseball», «rating»: [»2»,»2»], «location»:»45.12,-68.35»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Jeff», «birthdate»:»1990–4–1», «sport»: «Baseball», «rating»: [»2»,»3»], «location»:»46.12,-68.55»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Will», «birthdate»:»1988–3–1», «sport»: «Baseball», «rating»: [»4»,»4»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Mick», «birthdate»:»1989–10–1», «sport»: «Baseball», «rating»: [»3»,»4»], «location»:»46.22,-68.45»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Pong», «birthdate»:»1989–11–2», «sport»: «Baseball», «rating»: [»1»,»3»], «location»:»45.21,-68.35»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Ray», «birthdate»:»1988–10–3», «sport»: «Baseball», «rating»: [»2»,»2»], «location»:»45.16,-63.58» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Ping», «birthdate»:»1992–5–20», «sport»: «Baseball», «rating»: [»4»,»3»], «location»:»45.22,-68.53»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Duke», «birthdate»:»1992–2–28», «sport»: «Baseball», «rating»: [»5»,»2»], «location»:»46.22,-68.85»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Hal», «birthdate»:»1990–9–9», «sport»: «Baseball», «rating»: [»4»,»2»], «location»:»45.12,-68.35»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Charge», «birthdate»:»1990–4–1», «sport»: «Baseball», «rating»: [»3»,»2»], «location»:»46.12,-68.55»} {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Barry», «birthdate»:»1988–3–1», «sport»: «Baseball», «rating»: [»5»,»2»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Bank», «birthdate»:»1988–3–1», «sport»: «Golf», «rating»: [»6»,»4»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Bingo», «birthdate»:»1988–3–1», «sport»: «Golf», «rating»: [»10»,»7»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «James», «birthdate»:»1988–3–1», «sport»: «Basketball», «rating»: [»10»,»8»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Wayne», «birthdate»:»1988–3–1», «sport»: «Hockey», «rating»: [»10»,»10»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Brady», «birthdate»:»1988–3–1», «sport»: «Football», «rating»: [»10»,»10»], «location»:»46.25,-68.55» } {«index»:{»_index»: «sports»,»_type»: «athlete»}} {«name»: «Lewis», «birthdate»:»1988–3–1», «sport»: «Football», «rating»: [»10»,»10»], «location»:»46.25,-68.55» }' Давайте сгруппируем спортсменов по их виду спорта и узнаем сколько их в каждом спорте:
curl -XPOST «http://localhost:9200/sports/athlete/_search? pretty» -d' { «size»: 0, «aggregations»: { «the_name»: { «terms»: { «field»: «sport» } } } }' Тут мы используем агрегат «terms», который группирует документа по полю «sport».«size» : 0 (0 заменяется на Integer.MAX_VALUE автоматически) говорит о том, что нам нужные все документы без исключения, в нашем случае не важна скорость, но надо учитывать, что более точный результат требует больше времени.
Ответ:
{ … «aggregations» : { «the_name» : { «buckets» : [ { «key» : «baseball», «doc_count» : 16 }, { «key» : «golf», «doc_count» : 2 }, { «key» : «basketball», «doc_count» : 1 }, { «key» : «football», «doc_count» : 1 }, { «key» : «hockey», «doc_count» : 1 } ] } } } Отлично, бейсболистов больше всего.Давайте отсортируем спортсменов по среднему значению их рейтинга, от большего к меньшему:
curl -XPOST «http://localhost:9200/sports/athlete/_search? pretty» -d' { «size»: 0, «aggregations»: { «the_name»: { «terms»: { «field»: «name», «order»: { «rating_avg»: «desc» } }, «aggregations»: { «rating_avg»: { «avg»: { «field»: «rating» } } } } } }' Тут отлично видно, что такое вложенный агрегат и как он может помочь нам выбрать документы максимально гибко.Сначала мы указываем, что нужно сгруппировать спортсменов по имени, потом отсортировать по «rating_avg», который высчитывается в под агрегате «avg», по полю «rating». Заметьте, как элегантно ES работает с массивами («rating» : [10, 9]) и с легкостью высчитывает среднее значение.Ответ:
{ … «aggregations» : { «the_name» : { «buckets» : [ { «key» : «brady», «doc_count» : 1, «rating_avg» : { «value» : 10.0 } }, { «key» : «wayne», «doc_count» : 1, «rating_avg» : { «value» : 10.0 } }, { «key» : «james», «doc_count» : 1, «rating_avg» : { «value» : 9.0 } }, { «key» : «bingo», «doc_count» : 1, «rating_avg» : { «value» : 8.5 } }, … {} … { «key» : «duke», «doc_count» : 1, «rating_avg» : { «value» : 3.5 } }, { «key» : «bob», «doc_count» : 1, «rating_avg» : { «value» : 3.5 } } ] } } } Еще одна прекрасная возможность агрегатов это использование «script». Например:
curl -XPOST «http://localhost:9200/sports/athlete/_search? pretty» -d' { «size»: 0, «aggregations»: { «age_ranges»: { «range»: { «script»: «DateTime.now ().year — doc[\«birthdate\»].date.year», «ranges»: [ { «from»: 22, «to»: 25 } ] } } } }' Начиная с версии 1.2.0 выполнение скриптов по умолчанию отключено. Вы можете его включить, при условии что у пользователей нет прямого доступа к ES (Надеюсь, что это так, иначе советую вам немедленно закрыть этот доступ ради безопасности ваших данных).Агрегация во всей красе или что-то посложнее Давайте найдём всех спортсменов, которые находятся в радиусе 20 миль от точки »46.12,-68.55«Сгруппируем их по виду спорта и выведем подробную статистику по рейтингу спортсменов в этом виде спорта.Звучит неплохо, а вот и пример.
curl -XPOST «http://localhost:9200/sports/athlete/_search? pretty» -d' { «size»: 0, «aggregations»: { «baseball_player_ring»: { «geo_distance»: { «field»: «location», «origin»:»46.12,-68.55», «unit»: «mi», «ranges»: [ { «from»: 0, «to»: 20 } ] }, «aggregations»: { «sport»: { «terms»: { «field»: «sport» }, «aggregations»: { «rating_stats»: { «stats»: { «field»: «rating» } } } } } } } } }' Ответ:
{ … «aggregations» : { «baseball_player_ring» : { «buckets» : [ { «key» :»*-20.0», «from» : 0.0, «to» : 20.0, «doc_count» : 13, «sport» : { «buckets» : [ { «key» : «baseball», «doc_count» : 8, «rating_stats» : { «count» : 14, «min» : 2.0, «max» : 5.0, «avg» : 3.357142857142857, «sum» : 47.0 } }, { «key» : «golf», «doc_count» : 2, «rating_stats» : { «count» : 4, «min» : 4.0, «max» : 10.0, «avg» : 6.75, «sum» : 27.0 } }, { «key» : «basketball», «doc_count» : 1, «rating_stats» : { «count» : 2, «min» : 8.0, «max» : 10.0, «avg» : 9.0, «sum» : 18.0 } }, { «key» : «football», «doc_count» : 1, «rating_stats» : { «count» : 1, «min» : 10.0, «max» : 10.0, «avg» : 10.0, «sum» : 10.0 } }, { «key» : «hockey», «doc_count» : 1, «rating_stats» : { «count» : 1, «min» : 10.0, «max» : 10.0, «avg» : 10.0, «sum» : 10.0 } } ] } } ] } } } Заключение Надеюсь, я смог донести общие возможности этого прекрасного модуля. Всем, кого это тема заинтересовала, я советую ознакомиться со всем списком фильтров по этой ссылке.Рад любым полезным замечаниям и дополнениям по теме.Так же можно прочитать мою предыдущую статью по ES — ElasticSearch и поиск наоборот. Percolate APIИ принять участие в голосование внизу статьи.