在 Couchbase 中查询 MapReduce 视图
1. 概述
在本教程中,我们将介绍一些简单的 MapReduce 视图并演示如何使用Couchbase Java SDK 查询它们。
2.Maven依赖
要在 Maven 项目中使用 Couchbase,请将 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 map函数和可选的reduce函数定义的。
3.1. map功能
map函数针对每个文档运行一次。创建视图时,map函数对存储桶中的每个文档运行一次,结果存储在存储桶中。
创建视图后,map函数仅针对新插入或更新的文档运行,以增量更新视图。
因为map函数的结果存储在数据桶中,所以针对视图的查询表现出低延迟。
让我们看一个map函数的示例,该函数在存储桶中type字段等于*“StudentGrade”的所有文档的name*字段上创建索引:
function (doc, meta) {
if(doc.type == "StudentGrade" && doc.name) {
emit(doc.name, null);
}
}
emit函数告诉 Couchbase将哪些数据字段存储在索引键(第一个参数)中,以及与索引文档关联的值(第二个参数)。
在这种情况下,我们仅将文档name属性存储在索引键中。由于我们对将任何特定值与每个条目关联不感兴趣,因此我们将null作为 value 参数传递。
当 Couchbase 处理视图时,它会创建map函数发出的键的索引,将每个键与发出该键的所有文档相关联。
例如,如果三个文档的name属性设置为*“John Doe”,那么索引键“John Doe”*将与这三个文档相关联。
3.2. 减少函数_
reduce函数用于使用map函数的结果执行聚合计算。Couchbase Admin UI 提供了一种将内置reduce函数*“_count”、“_sum”和“_stats”应用到map*函数的简单方法。
您还可以为更复杂的聚合编写自己的reduce函数。我们将在本教程后面看到使用内置reduce函数的示例。
4. 使用视图和查询
4.1.组织视图
每个存储桶将视图组织成一个或多个设计文档。理论上,每个设计文档的视图数量没有限制。但是,为了获得最佳性能,建议您将每个设计文档限制为少于十个视图。
当您第一次在设计文档中创建视图时,Couchbase 将其指定为development 视图。您可以针对development 视图运行查询以测试其功能。一旦您对视图感到满意,您将publish 设计文档,该视图将成为production视图。
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,我们可以遍历行来获取文档 ID 和/或内容:
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
}
我们会将这些文档存储在“ blogdemo-tutorial ”存储桶中,并将所有视图存储在一个名为“ studentGrades ”的设计文档中。让我们看一下打开存储桶所需的代码,以便我们可以查询它:
Bucket bucket = CouchbaseCluster.create("127.0.0.1")
.openBucket("blogdemo-tutorial");
6. 精确匹配查询
假设您要查找特定课程或一组课程的所有学生成绩。让我们使用以下映射函数编写一个名为“ findByCourse ”的视图:
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. 涉及单个字段的查询
无论course 字段的值如何,要查找具有一系列grade 值的所有文档,我们需要一个仅发出grade 字段的视图。让我们为“ findByGrade ”视图编写map函数:
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);
要查找所有“A”等级(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 ]形式的数组发出。
让我们看一下“ findByCourseAndGrade ”视图的map函数:
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);
如果我们想找到所有数学成绩为“A”(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 在应用限制之前应用排序。
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来计算每个学生的 GPA:
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));
}