воскресенье, 16 февраля 2014 г.

Задачи на RavenDB. Часть 2: Злободневное про Олимпиаду.

Ну, не такое уж и злободневное. Просто сегодня уже наконец-то попробуем разобраться, зачем нам вся эта nosql-ная балалайка.
Выполнив все шаги из предыдущего поста, мы должны были получить базу данных Experiment1, в которой есть коллекция документов OlympicAthletes и 8618 записей. Как-то так:
initial state,
где документ выгладит так:
initial state document
У вас получилось похоже? Отлично! Если нет – задавайте вопросы, постараемся разобраться.
Тут возникла одна сложность: создание индексов в Management Studio достаточно наглядно, но не содержит простого способа, как работать с json-свойствами, имена которых состоят из нескольких слов (ну или я не знаю этого способа). Поэтому я поправил заголовки полей в исходной таблице, пожалуйста, повторите действия по импорту данных еще раз. Ссылка вот.
Тут надо заметить, что имена свойств из одного слова – это не фундаментальное ограничение RavenDB, а ограничение Management Studio, которая предназначена скорее для административных и демонстрационных целей, чем для настоящей работы. В .NET API эта проблема может быть разрешена весьма элегантным способом.
Сэмпл документа для нового импорта должен выглядеть так:
new doc sample

Постановка задачи.

На http://www.tableausoftware.com/public/community/sample-data-sets#medals нам предлагают попытаться узнать следующее (я попытаюсь переформулировать эти вопросы во что-то более конкретное):
  1. How many medals have athletes won since the 2000 Games? – Сколько всего медалей было получено начиная с 2000 года?
  2. How have the number of gold medals changed over time? – Сколько золотых медалей разыгрывалось на каждой Олимпиаде?
  3. Which countries have won the most number of medals in swimming? – Найти все страны, когда-либо получавшие медали в плавании, и отсортировать их по количеству медалей в плавании.

Как все это делается?

Найти ответы мы попробуем, пользуясь исключительно встроенной в RavenDB поддержкой Map/Reduce. Все, что нужно знать о  Map/Reduce, прекрасно изложил автор RavenDB: http://ayende.com/blog/4435/map-reduce-a-visual-explanation.
Работать будем в основном на вкладке Indexes в Raven Studio:
indexes overview
Заметьте, что Raven уже создал для нас один индекс. Но нам потребуются еще. Нажимаем New Index  и видим пустые поля для имени индекса и функции map:
empty index

И наконец-то код!

Начнем с вопроса №2: он несколько попроще и позволит продемонстрировать базовые концепции более наглядно.
“How have the number of gold medals changed over time?”
1. Индексу нужно имя. Пусть это будет Question2.
2. RavenDB является schemaless базой данных. Т.е. в отличие от всяких SQL (где есть колоночки и табличечки), Raven понятия не имеет, что же именно мы записываем внутрь наших документов. Рассказать ему об этом можно с помощью функции map.
Что нам потребуется, чтобы ответить на этот вопрос?

Map.

В документе хранится информация о том, сколько золотых медалей выиграл каждый спортсмен на какой-то конкретной Олимпиаде.  Если спортсмен выступал удачно на нескольких Олимпиадах, в коллекции будут документы для каждого выступления.
map
Функция Map – это Linq-запрос, который должен рассказать map-reduce движку, какие в принципе данные нам потребуются для анализа. Мы обращаемся ко всем документам как к docs и к интересующей нас коллекции по ее имени: docs.OlympicAthletes. Потом просим выбрать из каждого документа в коллекции новый анонимный объект, заявляя, что в каждом документе содержатся поля Year (год Олимпиады) и GoldMedals (сумма золотых медалей, выигранных в это выступлении конкретного спортсмена). Больше нам ничего не нужно.

Reduce.

Хорошо, теперь у нас есть коллекция, описывающая количество золотых медалей, полученных каждым спортсменом, выступавшим на конкретной Олимпиаде (она описывается годом проведения, и это нормально: в каждом конкретном году проходит лишь одна Олимпиада). Чтобы получить полное количество медалей на каждой Олимпиаде, нужно… сгруппировать элементы этой коллекции по году и найти сумму медалей! Это можно сделать с помощью нехитрого linq-запроса:
reduce
Синтаксис должен быть понятен для любого мало-мальски опытного разработчка: мы обращаемся в результату функции map как к коллекции по имени “results”, называем каждый конкретный элемент “result” и группируем их в аггрегацию “g” по полю Year. Затем выбираем новый анонимный объект с результатами, суммируя все золотые медали с помощью метода Sum. Тип объекта, возвращаемого в коллекции результатов Reduce, должен быть таким же, как и тип объекта, возвращаемого функцией Map. Почему именно так, хорошо описано в статье Ayende, ссылка на которую приведена выше. Если есть вопросы – пишите.
Ура, вроде все готово. Сохраняем индекс с помощью кнопки Save index (с дискеткой):
save index

Посмотрим, что получилось?

Для этого нужно вернуться на страничку со списком индексов, нажав на заголовок вкладки Indexes. Вот что мы там увидим:
indexes
Индекс под названием Question2 создался, начал выполняться и уже имеет некоторое количество результатов (у меня - 7). Индексирование происходит в фоне, и для более сложных индексов количество возвращаемых результатов будет со временем меняться.
Статус индексирования можно посмотреть на нижней строчке в Management studio:
statistics
Здесь указано полное количество документов в коллекции, количество индексов и количество индексов, для которых индексирование еще не завершено. Сейчас на моей машине все индексы уже готовы, и количество результатов (7) уже не изменится, пока не будет изменена сама коллекция.
Если нажать на имя индекса, откроется страница, позволяющая запрашивать этот индекс, используя синтаксис Lucene (RavenDB использует Lucene как подлежащий движок для индексирования результатов, полнотекстового поиска и прочих плюшек).
query
Кнопка Execute позволяет выполнить запрос. Результаты отображаются в окошке Results. Пустой запрос возвращает все вхождения текущего индекса.

Сортировка?

Все бы вроде хорошо, но хотелось бы отсортировать вывод по возрастанию года проведения Олимпиады. Это можно сделать с помощью кнопки Add Sort By.
add sort by
Нам подойдет опция Year. Выбираем ее и вот результаты:
results with sorting
Вот и ответ на заданный вопрос. Возможно, лучше бы было разделить задачу на две: для зимней (2002, 2006, 2010) и летней Олимпиады (2000, 2004, 2008, 2012), т.к. количество медалей сильно отличается. Такую задачу можно использовать как самостоятельное упражнение.
Вот идея того, как это можно сделать для летних Олимпиад. Создайте новый индекс (например, GoldMedalsForSummerGames), где функция map содержит некоторое ограничение, например:
from doc in docs.OlympicAthleteswhere (new List<int> {2000, 2004, 2008, 2012}).Contains(doc.Year) select new { Year = doc.Year, GoldMedals = doc.GoldMedals }

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

В следующий раз посмотрим на задачу №1.

Спасибо за внимание.

Комментариев нет:

Отправить комментарий