Перейти к основному содержимому

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

· 11 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Функция сокращения используется для выполнения агрегатных вычислений с использованием результатов функции карты . Пользовательский интерфейс Couchbase Admin предоставляет простой способ применить встроенные функции сокращения «_count», «_sum» и «_stats» к вашей функции карты .

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

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

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

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

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

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
}

Мы будем хранить эти документы в корзине « foreach-tutorial », а все представления — в проектном документе под названием « studentGrades ». Давайте посмотрим на код, необходимый для открытия ведра, чтобы мы могли запросить его:

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

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

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

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

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

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

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

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. Запросы, включающие одно поле

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

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

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

Давайте посмотрим на функцию карты для представления « findByCourseAndGrade »:

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

Когда это представление заполняется в Couchbase, записи указателя сортируются по курсу и оценке . Вот подмножество ключей в представлении « 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));

Обратите внимание: поскольку мы фиксируем значение курса на « Математика », мы должны включить верхнюю границу с максимально возможным значением оценки . В противном случае наш набор результатов также включал бы все документы, значение курса которых лексикографически больше, чем « Математика ».

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

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

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

Наконец, чтобы найти пять самых высоких оценок по математике (за исключением любых совпадений), вы можете указать 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 применяет сортировку до применения лимита .

8. Совокупные запросы

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

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

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

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

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

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» мы должны выдавать суммируемое значение — в данном случае количество кредитов — для каждой записи.

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

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 балл за кредитный час).

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

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

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

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 , чтобы рассчитать средний балл для каждого студента:

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 и о том, как запрашивать их в Java , можно на официальном сайте документации для разработчиков Couchbase .