Заявка за Couchbase с MapReduce Views

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 Views

В Couchbase изгледът MapReduce е вид индекс, който може да се използва за заявка на група данни. Той се дефинира с помощта на JavaScript карта функция и незадължителна функция за намаляване .

3.1. На картата функция

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

След като се създаде изглед, функцията за карта се изпълнява само срещу нововмъкнати или актуализирани документи, за да се актуализира изгледа постепенно.

Тъй като резултатите от функцията за карта се съхраняват в групата с данни, заявките срещу изглед показват ниска латентност.

Нека разгледаме пример за функция на картата, която създава индекс в полето за име на всички документи в групата, чието поле на типа е равно на „StudentGrade“ :

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

Функцията за излъчване казва на Couchbase кои полета за данни да се съхраняват в ключа на индекса (първи параметър) и каква стойност (втори параметър) да се асоциират с индексирания документ.

В този случай ние съхраняваме само свойството на името на документа в ключа на индекса. И тъй като не се интересуваме от асоцииране на някаква конкретна стойност с всеки запис, ние предаваме null като параметър на стойността.

Докато Couchbase обработва изгледа, той създава индекс на ключовете, които се излъчват от функцията map , свързвайки всеки ключ с всички документи, за които е издаден този ключ.

Например, ако три документа имат свойството за име, зададено на „John Doe“ , тогава индексният ключ „John Doe“ ще бъде свързан с тези три документа.

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 }

We will store these documents in the “baeldung-tutorial” bucket and all views in a design document named “studentGrades.” Let's look at the code needed to open the bucket so that we can query it:

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

6. Exact Match Queries

Suppose you want to find all student grades for a particular course or set of courses. Let's write a view called “findByCourse” using the following map function:

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

Note that in this simple view, we only need to emit the course field.

6.1. Matching on a Single Key

To find all grades for the History course, we apply the key method to our base query:

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

6.2. Matching on Multiple Keys

If you want to find all grades for Math and Science courses, you can apply the keys method to the base query, passing it an array of key values:

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

7. Range Queries

In order to query for documents containing a range of values for one or more fields, we need a view that emits the field(s) we are interested in, and we must specify a lower and/or upper bound for the query.

Let's take a look at how to perform range queries involving a single field and multiple fields.

7.1. Queries Involving a Single Field

To find all documents with a range of grade values regardless of the value of the course field, we need a view that emits only the grade field. Let's write the map function for the “findByGrade” view:

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

Let's write a query in Java using this view to find all grades equivalent to a “B” letter grade (80 to 89 inclusive):

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

Note that the start key value in a range query is always treated as inclusive.

And if all the grades are known to be integers, then the following query will yield the same results:

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

To find all “A” grades (90 and above), we only need to specify the lower bound:

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

And to find all failing grades (below 60), we only need to specify the upper bound:

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

7.2. Queries Involving Multiple Fields

Now, suppose we want to find all students in a specific course whose grade falls into a certain range. This query requires a new view that emits both the course and grade fields.

With multi-field views, each index key is emitted as an array of values. Since our query involves a fixed value for course and a range of grade values, we will write the map function to emit each key as an array of the form [course, grade].

Let's look at the map function for the view “findByCourseAndGrade“:

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

When this view is populated in Couchbase, the index entries are sorted by course and grade. Here's a subset of keys in the “findByCourseAndGrade” view shown in their natural sort order:

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

Since the keys in this view are arrays, you would also use arrays of this format when specifying the lower and upper bounds of a range query against this view.

This means that in order to find all students who got a “B” grade (80 to 89) in the Math course, you would set the lower bound to:

["Math", 80]

and the upper bound to:

["Math", 89]

Let's write the range query in Java:

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

If we want to find for all students who received an “A” grade (90 and above) in Math, then we would write:

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

Note that because we are fixing the course value to “Math“, we have to include an upper bound with the highest possible grade value. Otherwise, our result set would also include all documents whose course value is lexicographically greater than “Math“.

And to find all failing Math grades (below 60):

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

Much like the previous example, we must specify a lower bound with the lowest possible grade. Otherwise, our result set would also include all grades where the course value is lexicographically less than “Math“.

Finally, to find the five highest Math grades (barring any ties), you can tell Couchbase to perform a descending sort and to limit the size of the result set:

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

Note that when performing a descending sort, the startKey and endKey values are reversed, because Couchbase applies the sort before it applies the limit.

8. Aggregate Queries

A major strength of MapReduce views is that they are highly efficient for running aggregate queries against large datasets. In our student grades dataset, for example, we can easily calculate the following aggregates:

  • number of students in each course
  • sum of credit hours for each student
  • grade point average for each student across all courses

Let's build a view and query for each of these calculations using built-in reduce functions.

8.1. Using the count() Function

First, let's write the map function for a view to count the number of students in each course:

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

We'll call this view “countStudentsByCourse” and designate that it is to use the built-in “_count” function. And since we are only performing a simple count, we can still emit null as the value for each entry.

To count the number of students in the each course:

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

Extracting data from aggregate queries is different from what we've seen up to this point. Instead of extracting a matching Couchbase document for each row in the result, we are extracting the aggregate keys and results.

Let's run the query and extract the counts into a java.util.Map:

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. Using the sum() Function

Next, let's write a view that calculates the sum of each student's credit hours attempted. We'll call this view “sumHoursByStudent” and designate that it is to use the built-in “_sum” function:

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

Note that when applying the “_sum” function, we have to emit the value to be summed — in this case, the number of credits — for each entry.

Let's write a query to find the total number of credits for each student:

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

And now, let's run the query and extract the aggregated sums into a java.util.Map:

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. Calculating Grade Point Averages

Suppose we want to calculate each student's grade point average (GPA) across all courses, using the conventional grade point scale based on the grades obtained and the number of credit hours that the course is worth (A=4 points per credit hour, B=3 points per credit hour, C=2 points per credit hour, and D=1 point per credit hour).

There is no built-in reduce function to calculate average values, so we'll combine the output from two views to compute the GPA.

We already have the “sumHoursByStudent” view that sums the number of credit hours each student attempted. Now we need the total number of grade points each student earned.

Let's create a view called “sumGradePointsByStudent” that calculates the number of grade points earned for each course taken. We'll use the built-in “_sum” function to reduce the following map function:

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); } } }

Now let's query this view and extract the sums into a java.util.Map:

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); }

Finally, let's combine the two Maps in order to calculate GPA for each student:

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. Conclusion

We have demonstrated how to write some basic MapReduce views in Couchbase, and how to construct and execute queries against the views, and extract the results.

The code presented in this tutorial can be found in the GitHub project.

Можете да научите повече за изгледите MapReduce и как да ги заявявате в Java на официалния сайт за документация за разработчици на Couchbase.