Рубрики
Без рубрики

Запрос Couchbase с представлениями MapReduce

Краткое введение в использование представлений MapReduce для запроса Couchbase.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы представим некоторые простые представления MapReduce и продемонстрируем, как запрашивать их с помощью Couchbase Java SDK .

2. Зависимость Maven

Чтобы работать с Couchbase в проекте Maven, импортируйте Couchbase SDK в свой pom.xml :


    com.couchbase.client
    java-client
    2.4.0

Вы можете найти последнюю версию на Maven Central .

3. Представления MapReduce

В Couchbase представление MapReduce-это тип индекса, который можно использовать для запроса корзины данных. Он определяется с помощью функции JavaScript map и необязательной функции reduce .

3.1. Функция карты

Функция map выполняется для каждого документа один раз. При создании представления функция map выполняется один раз для каждого документа в корзине, и результаты сохраняются в корзине.

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

Поскольку результаты функции map хранятся в корзине данных, запросы к представлению демонстрируют низкую задержку.

Давайте рассмотрим пример функции map , которая создает индекс в поле name всех документов в корзине, чье поле type равно “StudentGrade” :

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.name) {    
        emit(doc.name, null);
    }
}

Функция emit сообщает Couchbase, какие поля данных хранить в индексном ключе(первый параметр) и какое значение (второй параметр) связывать с индексированным документом.

В этом случае мы сохраняем только свойство document name в индексном ключе. И поскольку мы не заинтересованы в связывании какого-либо конкретного значения с каждой записью, мы передаем null в качестве параметра value.

Когда Couchbase обрабатывает представление, он создает индекс ключей, испускаемых функцией map , связывая каждый ключ со всеми документами, для которых этот ключ был испущен.

Например, если три документа имеют свойство name , установленное в “John Doe” , то ключ индекса “John Doe” будет связан с этими тремя документами.

3.2. Функция уменьшения

Функция reduce используется для выполнения агрегированных вычислений с использованием результатов функции map . Пользовательский интерфейс администратора Couchbase предоставляет простой способ применить встроенные функции reduce | “_count”, “_sum”, и “_stats” к вашей функции map .

Вы также можете написать свои собственные функции reduce для более сложных агрегаций. Ниже мы рассмотрим примеры использования встроенных функций reduce .

4. Работа С Представлениями и Запросами

4.1. Организация представлений

Представления организуются в один или несколько проектных документов на ведро. Теоретически количество просмотров на проектный документ не ограничено. Однако для достижения оптимальной производительности было предложено ограничить каждый проектный документ менее чем десятью видами.

Когда вы впервые создаете представление в проектном документе, Couchbase определяет его как представление development . Вы можете запускать запросы к представлению development , чтобы проверить его функциональность. Как только вы удовлетворены представлением, вы публикуете проектный документ, и представление становится производственным представлением.

4.2. Построение запросов

Чтобы построить запрос к представлению Couchbase, необходимо указать его имя конструкторского документа и имя представления для создания объекта ViewQuery :

ViewQuery query = ViewQuery.from("design-document-name", "view-name");

При выполнении этого запроса будут возвращены все строки представления. В последующих разделах мы увидим, как ограничить результирующий набор на основе ключевых значений.

Чтобы построить запрос к представлению разработки, можно применить метод development() при создании запроса:

ViewQuery query 
  = ViewQuery.from("design-doc-name", "view-name").development();

4.3. Выполнение запроса

Как только у нас есть объект View Query , мы можем выполнить запрос, чтобы получить результат View :

ViewResult result = bucket.query(query);

4.4. Обработка Результатов Запроса

И теперь , когда у нас есть View Result , мы можем перебирать строки, чтобы получить идентификаторы документов и/или содержимое:

for(ViewRow row : result.allRows()) {
    JsonDocument doc = row.document();
    String id = doc.id();
    String json = doc.content().toString();
}

5. Пример приложения

В оставшейся части урока мы напишем представления MapReduce и запросы для набора документов оценок учащихся, имеющих следующий формат, с оценками, ограниченными диапазоном от 0 до 100:

{ 
    "type": "StudentGrade",
    "name": "John Doe",
    "course": "History",
    "hours": 3,
    "grade": 95
}

Мы будем хранить эти документы в ” baeldung-tutorial “bucket и все представления в проектном документе с именем” student Grades .” Давайте посмотрим на код, необходимый для открытия корзины, чтобы мы могли запросить его:

Bucket bucket = CouchbaseCluster.create("127.0.0.1")
  .openBucket("baeldung-tutorial");

6. Точные Запросы Соответствия

Предположим, вы хотите найти все оценки студентов по определенному курсу или набору курсов. Давайте напишем представление под названием ” найти по курсу “, используя следующую функцию map :

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.grade) {
        emit(doc.course, null);
    }
}

Обратите внимание, что в этом простом представлении нам нужно только испустить поле course .

6.1. Совпадение по одному ключу

Чтобы найти все оценки по курсу истории, мы применяем метод key к нашему базовому запросу:

ViewQuery query 
  = ViewQuery.from("studentGrades", "findByCourse").key("History");

6.2. Сопоставление по нескольким ключам

Если вы хотите найти все оценки по математике и естественным наукам, вы можете применить метод keys к базовому запросу, передав ему массив ключевых значений:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourse")
  .keys(JsonArray.from("Math", "Science"));

7. Запросы диапазона

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

Давайте рассмотрим, как выполнять запросы диапазона, включающие одно поле и несколько полей.

7.1. Запросы, включающие одно поле

Чтобы найти все документы с диапазоном значений grade независимо от значения поля course , нам нужно представление, которое выдает только поле grade . Давайте напишем функцию map для представления ” find By Grade “:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.grade) {
        emit(doc.grade, null);
    }
}

Давайте напишем запрос на Java, используя это представление, чтобы найти все оценки, эквивалентные букве “B” (от 80 до 89 включительно):

ViewQuery query = ViewQuery.from("studentGrades", "findByGrade")
  .startKey(80)
  .endKey(89)
  .inclusiveEnd(true);

Обратите внимание, что начальное значение ключа в запросе диапазона всегда рассматривается как включающее.

И если все оценки известны как целые числа, то следующий запрос даст те же результаты:

ViewQuery query = ViewQuery.from("studentGrades", "findByGrade")
  .startKey(80)
  .endKey(90)
  .inclusiveEnd(false);

Чтобы найти все оценки “А” (90 и выше), нам нужно только указать нижнюю границу:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByGrade")
  .startKey(90);

И чтобы найти все неудачные оценки (ниже 60), нам нужно только указать верхнюю границу:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByGrade")
  .endKey(60)
  .inclusiveEnd(false);

7.2. Запросы, Включающие Несколько Полей

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

При многопольных представлениях каждый ключ индекса выдается в виде массива значений. Поскольку наш запрос включает фиксированное значение для course и диапазон значений grade , мы напишем функцию map для выделения каждого ключа в виде массива вида [ course , grade ].

Давайте посмотрим на функцию map for the view ” find By Course And Grade “:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.grade) {
        emit([doc.course, doc.grade], null);
    }
}

Когда это представление заполняется в Couchbase, записи индекса сортируются по курсу и классу . Вот подмножество ключей в представлении ” найти по курсу и классу “, показанное в их естественном порядке сортировки:

["History", 80]
["History", 90]
["History", 94]
["Math", 82]
["Math", 88]
["Math", 97]
["Science", 78]
["Science", 86]
["Science", 92]

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

Это означает, что для того, чтобы найти всех студентов, получивших оценку “В” (от 80 до 89) по математике, вы должны установить нижнюю границу в:

["Math", 80]

и верхняя граница к:

["Math", 89]

Давайте напишем запрос диапазона на Java:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .startKey(JsonArray.from("Math", 80))
  .endKey(JsonArray.from("Math", 89))
  .inclusiveEnd(true);

Если бы мы хотели найти для всех студентов, получивших оценку “А” (90 и выше) по математике, то мы бы написали:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .startKey(JsonArray.from("Math", 90))
  .endKey(JsonArray.from("Math", 100));

Обратите внимание, что поскольку мы фиксируем значение курса на ” Math “, мы должны включить верхнюю границу с максимально возможным значением grade . В противном случае наш результирующий набор также включал бы все документы, значение course которых лексикографически больше, чем ” Math “.

И найти все провальные оценки по математике (ниже 60):

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .startKey(JsonArray.from("Math", 0))
  .endKey(JsonArray.from("Math", 60))
  .inclusiveEnd(false);

Как и в предыдущем примере, мы должны указать нижнюю границу с наименьшей возможной оценкой. В противном случае наш результирующий набор также включал бы все оценки, где значение course лексикографически меньше, чем ” Math “.

Наконец, чтобы найти пять самых высоких математических оценок (за исключением каких-либо связей), вы можете сказать Couchbase выполнить сортировку по убыванию и ограничить размер результирующего набора:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .descending()
  .startKey(JsonArray.from("Math", 100))
  .endKey(JsonArray.from("Math", 0))
  .inclusiveEnd(true)
  .limit(5);

Обратите внимание, что при выполнении сортировки по убыванию значения startKey и EndKey меняются местами, поскольку Couchbase применяет сортировку до того, как применит limit .

8. Агрегированные запросы

Основная сила представлений MapReduce заключается в том, что они очень эффективны для выполнения агрегированных запросов к большим наборам данных. Например, в нашем наборе данных оценок учащихся мы можем легко вычислить следующие агрегаты:

  • количество студентов на каждом курсе
  • сумма зачетных часов для каждого студента
  • средний балл оценки для каждого студента по всем курсам

Давайте построим представление и запрос для каждого из этих вычислений с помощью встроенных функций reduce .

8.1. Использование графа() Функция

Во-первых, давайте напишем функцию map для представления, чтобы подсчитать количество студентов на каждом курсе:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.name) {
        emit([doc.course, doc.name], null);
    }
}

Мы назовем это представление ” count Students By Course ” и обозначим, что оно должно использовать встроенную функцию “_count” . И поскольку мы выполняем только простой подсчет, мы все равно можем выдавать null в качестве значения для каждой записи.

Подсчитать количество студентов на каждом курсе:

ViewQuery query = ViewQuery
  .from("studentGrades", "countStudentsByCourse")
  .reduce()
  .groupLevel(1);

Извлечение данных из агрегированных запросов отличается от того, что мы видели до этого момента. Вместо извлечения соответствующего документа Couchbase для каждой строки в результате мы извлекаем совокупные ключи и результаты.

Давайте запустим запрос и извлекем подсчеты в файл java.util.Карта :

ViewResult result = bucket.query(query);
Map numStudentsByCourse = new HashMap<>();
for(ViewRow row : result.allRows()) {
    JsonArray keyArray = (JsonArray) row.key();
    String course = keyArray.getString(0);
    long count = Long.valueOf(row.value().toString());
    numStudentsByCourse.put(course, count);
}

8.2. Использование функции sum()

Затем давайте напишем представление, которое вычисляет сумму зачетных часов каждого студента. Мы назовем это представление ” несколько часов студентом ” и обозначим, что оно должно использовать встроенную функцию “_sum” :

function (doc, meta) {
    if(doc.type == "StudentGrade"
         && doc.name
         && doc.course
         && doc.hours) {
        emit([doc.name, doc.course], doc.hours);
    }
}

Обратите внимание, что при применении функции “_sum” мы должны выдавать значение, которое должно быть суммировано — в данном случае количество кредитов — для каждой записи.

Давайте напишем запрос, чтобы найти общее количество кредитов для каждого студента:

ViewQuery query = ViewQuery
  .from("studentGrades", "sumCreditsByStudent")
  .reduce()
  .groupLevel(1);

А теперь давайте запустим запрос и извлекем агрегированные суммы в файл java.util.Карта :

ViewResult result = bucket.query(query);
Map hoursByStudent = new HashMap<>();
for(ViewRow row : result.allRows()) {
    String name = (String) row.key();
    long sum = Long.valueOf(row.value().toString());
    hoursByStudent.put(name, sum);
}

8.3. Расчет Среднего Балла

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

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

У нас уже есть представление “sumHoursByStudent” , которое суммирует количество кредитных часов, потраченных каждым студентом. Теперь нам нужно общее количество баллов, заработанных каждым учеником.

Давайте создадим представление под названием “sumGradePointsByStudent” , которое вычисляет количество баллов, заработанных за каждый пройденный курс. Мы будем использовать встроенную функцию “_sum” для уменьшения следующей функции map :

function (doc, meta) {
    if(doc.type == "StudentGrade"
         && doc.name
         && doc.hours
         && doc.grade) {
        if(doc.grade >= 90) {
            emit(doc.name, 4*doc.hours);
        }
        else if(doc.grade >= 80) {
            emit(doc.name, 3*doc.hours);
        }
        else if(doc.grade >= 70) {
            emit(doc.name, 2*doc.hours);
        }
        else if(doc.grade >= 60) {
            emit(doc.name, doc.hours);
        }
        else {
            emit(doc.name, 0);
        }
    }
}

Теперь давайте запросим это представление и извлекем суммы в файл java.util.Карта :

ViewQuery query = ViewQuery.from(
  "studentGrades",
  "sumGradePointsByStudent")
  .reduce()
  .groupLevel(1);
ViewResult result = bucket.query(query);

Map gradePointsByStudent = new HashMap<>();
for(ViewRow row : result.allRows()) {
    String course = (String) row.key();
    long sum = Long.valueOf(row.value().toString());
    gradePointsByStudent.put(course, sum);
}

Наконец, давайте объединим две карты s, чтобы рассчитать средний балл для каждого студента:

Map result = new HashMap<>();
for(Entry creditHoursEntry : hoursByStudent.entrySet()) {
    String name = creditHoursEntry.getKey();
    long totalHours = creditHoursEntry.getValue();
    long totalGradePoints = gradePointsByStudent.get(name);
    result.put(name, ((float) totalGradePoints / totalHours));
}

9. Заключение

Мы продемонстрировали, как написать некоторые базовые представления MapReduce в Couchbase, а также как создавать и выполнять запросы к этим представлениям и извлекать результаты.

Код, представленный в этом учебнике, можно найти в проекте GitHub .

Вы можете узнать больше о представлениях MapReduce и о том, как запрашивать их в Java на официальном сайте документации разработчика Couchbase .