Запрос Couchbase с помощью представлений MapReduce

1. Обзор

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

2. Maven Dependency

Для работы с Couchbase в проекте Maven импортируйте Couchbase SDK в свой pom.xml :

<dependency>
    <groupId>com.couchbase.client</groupId>
    <artifactId>java-client</artifactId>
    <version>2.4.0</version>
</dependency>

Вы можете найти последнюю версию на https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.couchbase.client%22%20AND%20a%3A%22java-client%22 [Maven Central.

3. MapReduce Просмотры

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

3.1. Функция map

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

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

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

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

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

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

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

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

Например, если для трех документов свойство name установлено в «Джон Доу» , то индексный ключ «Джон Доу» будет связан с этими тремя документами.

3.2. Функция reduce

Функция 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. Выполнение запроса

Получив объект ViewQuery , мы можем выполнить запрос, чтобы получить ViewResult :

ViewResult result = bucket.query(query);

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

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

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 », ​​а все представления - в проектном документе с именем « studentGrades ». Давайте рассмотрим код, необходимый для открытия корзины, чтобы мы могли запросить ее:

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

6. Точные совпадения запросов

Предположим, вы хотите найти все оценки учащихся для определенного курса или набора курсов. Давайте напишем представление под названием « findByCourse », используя следующую функцию 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 для представления « findByGrade »:

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 для представления « findByCourseAndGrade »:

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

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

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

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

Это означает, что для того, чтобы найти всех студентов, которые получили оценку «B» (от 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. Использование функции count ()

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

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

Мы назовем это представление « countStudentsByCourse » и обозначим, что оно должно использовать встроенную функцию « count» . И поскольку мы выполняем только простой подсчет, мы все равно можем выдавать null__ в качестве значения для каждой записи.

Чтобы подсчитать количество студентов в каждом курсе:

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

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

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

ViewResult result = bucket.query(query);
Map<String, Long> 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 ()

Далее, давайте напишем представление, которое вычисляет сумму попыток кредита каждого студента. Мы назовем это представление « sumHoursByStudent » и обозначим, что оно должно использовать встроенную функцию « sum» __:

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

Обратите внимание, что при применении функции « sum» мы должны emit__ суммировать значение - в данном случае количество кредитов - для каждой записи.

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

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

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

ViewResult result = bucket.query(query);
Map<String, Long> 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. Расчет средних баллов

Предположим, что мы хотим рассчитать средний балл каждого студента (GPA) по всем курсам, используя обычную шкалу баллов, основываясь на полученных оценках и количестве кредитных часов, которые стоит курс (A = 4 балла за кредитный час, B = 3 балла за кредитный час, C = 2 балла за кредитный час и D = 1 балл за кредитный час).

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

У нас уже есть представление «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.Map :

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

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

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

Map<String, Float> result = new HashMap<>();
for(Entry<String, Long> 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 views и о том, как query их в Java на официальном https://developer.couchbase.com/documentation/server/current/sdk/development-intro.html Сайт документации для разработчиков Couchbase.