В прошлый раз мы рассмотрели некоторые основы RethinkDB: его установку, запросы, вставку документов — все с помощью драйвера Ruby. В этой статье мы более подробно рассмотрим некоторые функции RethinkDB. Прежде чем мы начнем, у вас должна быть установлена и запущена копия RethinkDB.
Уменьшение карты
Скажем, у нас есть куча данных, распределенных по группе узлов. Мы пытаемся выполнить какое-то вычисление для этих данных; как мы должны это сделать? Одно очевидное решение (это, очевидно, тоже плохо) — попытка переместить все данные в один узел, где выполняется алгоритм, чтобы получить необходимую нам информацию. Есть много причин, почему это ужасная идея:
- У нас может не хватить памяти на одном узле
- Это займет много пропускной способности и т. Д.
Итак, как нам воспользоваться всеми этими узлами? Хорошо, напишите алгоритм, чтобы он мог работать параллельно в кластере. К сожалению, без каких-либо «рекомендаций» это может быть довольно сложной задачей. Например, что если один из узлов выйдет из строя во время работы нашего алгоритма? Как разделить набор данных так, чтобы все узлы тянули свой вес?
В те времена, когда исследователи / разработчики из Google сталкивались с этими проблемами, они разработали фреймворк MapReduce. По сути, MapReduce вынуждает вас определенным образом структурировать ваш алгоритм и, в свою очередь, может обрабатывать сбои системы и т. Д. Для вас. С MapReduce ваш код остается неизменным независимо от того, работает он на одном узле или на тысяче. Оказывается, RethinkDB включает в себя реализацию MapReduce, которая позволяет вам эффективно применять вычисления к вашему набору данных.
Итак, как работает MapReduce? Скажем, мы пытаемся оперировать множеством фрагментов данных и помещаем их все в список. Парадигма MapReduce состоит из двух этапов (RethinkDB вводит отдельный этап, называемый группировкой, который мы обсудим позже): сопоставление и уменьшение. Карта, как и метод Ruby map
, берет список, оперирует им, а затем выплевывает другой список. Reduce, как и в Ruby, берет список и «сводит» его к значению. Если вы напишите свой алгоритм с использованием этих «карт» и «уменьшенных» частей, RethinkDB выяснит, как эффективно разделить вычисления (по таблицам и осколкам).
Counting
Давайте возьмем очень простой пример, очень похожий на документацию , которая (к сожалению) фокусируется на Python. Помните стол «люди», который мы сделали в прошлый раз ? Конечно, нет. Итак, давайте соберем это снова. Помните, что для любых команд irb
отмеченных здесь, должно быть следующее:
require 'rethinkdb' include RethinkDB::Shortcuts conn = r.connect(:host => "localhost", :port => 28015)
Хорошо, давайте составим таблицу и добавим в нее кое-что:
r.db_create("testdb").run(conn) r.db("testdb").table_create("people").run(conn) r.db("testdb").table("people").insert({:name => "Dhaivat", :age => 18, :gender => "M"}).run(conn) r.db("testdb").table("people").insert({:name => "John", :age => 27, :gender => "M"}).run(conn) r.db("testdb").table("people").insert({:name => "Jane", :age => 94, :gender => "F"}).run(conn)
Некоторые из этих вызовов могут потерпеть неудачу, если вы уже правильно настроили структуру внутри RethinkDB.
Хорошо, скажем, мы хотим посчитать количество документов в таблице people
. Как мы можем сделать это с помощью map-Reduce (реализация RethinkDB упоминается в нижнем регистре; реализация Google — «MapReduce»)? Наша операция отображения получит в качестве входных данных каждый элемент списка, содержащий каждый документ в нашей таблице. Мы возьмем это и преобразуем в список единиц, а затем операция сокращения суммирует их и выдает одно число. Это довольно просто в коде:
map_op = Proc.new { |person| 1 } reduce_op = Proc.new { |val_a, val_b| val_a + val_b } r.db("testdb").table("people").map(map_op).reduce(reduce_op).run(conn)
Это должно красиво выплевать «3» (если, конечно, у вас есть три документа в таблице). Первые две строки — просто старый Ruby с точки зрения синтаксиса, но есть пара вещей, на которые стоит обратить внимание. Как написано, map_op
принимает только один аргумент: операция map предназначена для отображения каждого элемента в последовательности на один элемент в результирующей последовательности. reduce_op
берет два из 1, сгенерированных map
и складывает их вместе. Затем мы создаем запрос, передавая процы для map
и reduce
соответственно.
Что если бы мы вместо этого хотели выяснить сумму возрастов? Просто:
map_op = Proc.new { |person| person["age"] } reduce_op = Proc.new { |val_a, val_b| val_a + val_b } r.db("testdb").table("people").map(map_op).reduce(reduce_op).run(conn)
По сути, единственная разница в том, что map
возвращает возраст вместо 1
. Оказывается, существует значительно более простой способ решения обеих этих задач: методы count
и sum
. Взглянем:
r.db('testdb').table('people').count().run(conn) r.db('testdb').table('people').sum("age").run(conn)
Предполагается, что мы используем запросы сокращения карт, когда не можем найти что-то эквивалентное в существующем API . Парадигма сокращения карт невероятно мощна, но отчасти лишняя боль, если мы сможем найти что-то, что уже выполняет свою работу — вы будете удивлены, насколько далеко вы можете count
, sum
, avg
и filter
.
Группировка
Давайте немного перемотаем. Шаг карты получает каждый элемент в списке / последовательности. Что если мы хотим сгруппировать элементы особым образом? Например, может быть, мы хотим посчитать количество женщин и мужчин. Вот где мы используем группировку. Лучший способ узнать это — увидеть это в действии:
group_op = Proc.new { |person| person['gender'] } r.db('testdb').table('people').group(group_op).count.run(conn)
group_op
возвращает ключ для каждого элемента в последовательности, и каждый элемент группируется в соответствии с его ключом. В этом случае мы создаем группы на основе пола, поэтому мы используем person['gender']
в качестве ключа. Затем мы используем функцию count для подсчета элементов в каждой группе. Результат должен выглядеть так:
{"F"=>1, "M"=>2}
Если вы не хотите использовать служебную функцию count
, мы можем использовать те же самые map_op
и reduce_op
мы определили ранее:
map_op = Proc.new { |person| 1 } reduce_op = Proc.new { |val_a, val_b| val_a + val_b } r.db("testdb").table("people").group(group_op). map(map_op).reduce(reduce_op).run(conn)
Вместо того, чтобы работать с каждым элементом в последовательности / списке, map_op
теперь работает с каждым элементом в каждой группе. Затем, reduce_op
уменьшает каждую группу до одного значения, которое мы видим в выводе.
Hadoop
Если вы являетесь пользователем Hadoop или слышали о Hadoop, у вас может возникнуть вопрос: чем отличается карта-уменьшение на RethinkDB от того, что есть в Hadoop? Ну, одной из основных причин является то, что Hadoop предназначен для неструктурированных «больших» данных. Распределенная файловая система Hadoop (HDFS) настроена для одновременного чтения блоков размером 64 МБ и ориентирована на доступ к файлам. С другой стороны, RethinkDB является хранилищем документов и «понимает» структуру документов. Как правило, RethinkDB следует использовать в качестве бэкэнда, в который вы постоянно помещаете вещи и запрашиваете их (например, напрямую подключены к веб-приложению), а Hadoop можно использовать для пакетной обработки данных, сохраненных в RethinkDB.
присоединяется
Давайте переключать передачи полностью. Если вы ранее использовали какую-либо базу данных SQL, вы, вероятно, знакомы с «объединениями». Они берут информацию из двух отдельных таблиц и объединяют их, чтобы получить набор информации, которая имеет компоненты из обеих таблиц. Мы видим их чаще всего в отношениях между вещами, которые мы храним. Например, если у каждого человека может быть много домашних животных, это отношение один-ко-многим, когда мы можем использовать соединение за столом. Это такой же хороший пример, как и любой другой, поэтому мы рассмотрим, как его смоделировать в RethinkDB. Давайте сначала создадим правильную таблицу:
r.db('testdb').table_create('pets').run(conn)
Получите идентификаторы документов в таблице «люди»:
itr = r.db('testdb').table('people').run(conn) itr.each do |document| p document["id"] end
Выберите один из этих идентификаторов. Мы создадим питомца с владельцем из таблицы людей:
person_id = <picked id goes here> r.db("testdb").table("pets"). insert({:name => "Bob", :animal => "dog", :person_id => person_id}).run(conn)
У каждого человека может быть один или несколько домашних животных. Таким образом, каждый питомец должен иметь владельца и может хранить id
этого владельца. В итоге у нас есть поле с именем person_id внутри каждого домашнего животного. Мы можем создать соединение следующим образом:
r.db('testdb').table("pets"). eq_join("person_id", r.db("testdb").table("people")).run(conn)
Здесь мы используем eq_join("person_id", r.db("testdb").table("people"))
. Это говорит RethinkDB, что мы пытаемся подключить person_id
в таблице «pets» к первичному ключу в таблице «people». Это должно выплюнуть что-то похожее на это:
[{"left"=> {"animal"=>"dog", "id"=>"bfc9d1c1-442a-40d0-a58c-93641fe45ffb", "name"=>"Bob", "person_id"=>"6cf24b3d-aa75-4e8c-be88-868c74099ded"}, "right"=> {"age"=>94, "id"=>"6cf24b3d-aa75-4e8c-be88-868c74099ded", "name"=>"Jane"}}]>
«Левая» сторона представляет сторону, которая содержит соответствующий идентификатор (в данном случае person_id
), а «правая» содержит другую сторону. Мы можем сгладить представление довольно легко:
r.db('testdb').table("pets"). eq_join("person_id", r.db("testdb").table("people")). zip.run(conn)
Все, что мы сделали, — это позвонили в zip
. Теперь мы получаем итератор, представляющий:
[{"age"=>94, "animal"=>"dog", "id"=>"6cf24b3d-aa75-4e8c-be88-868c74099ded", "name"=>"Jane", "person_id"=>"6cf24b3d-aa75-4e8c-be88-868c74099ded"}]
Потрясающие! Мы в основном объединили информацию из двух таблиц, как мы надеялись, с объединением.
Завершение
Надеюсь, вам понравился этот обзор некоторых функций RethinkDB (и пару сравнений с другими системами). В следующем посте мы рассмотрим, как создать веб-приложение, используя RethinkDB в качестве бэкэнда.
Пожалуйста, оставьте вопросы в разделе комментариев ниже.