使用MapReduce视图查询Couchbase

2025/04/18

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.7.2</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在索引键中存储哪些数据字段(第一个参数)以及与索引文档关联的值(第二个参数)。

在本例中,我们只在索引键中存储文档名称属性。由于我们不想将任何特定值与每个条目关联,因此我们将null作为值参数传递。

当Couchbase处理视图时,它会创建由map函数发出的键的索引,并将每个键与发出该键的所有文档关联起来。

例如,如果3个文档的name属性设置为“John Doe”,则索引键“John Doe”将与这3个文档相关联。

3.2 reduce函数

Reduce函数用于对Map函数的结果进行聚合计算,Couchbase管理界面提供了一种简单的方法,可以将内置的Reduce函数“_count”、“_sum”和“_stats”应用于Map函数。

你还可以编写自己的Reduce函数来实现更复杂的聚合,本教程后面将介绍如何使用内置Reduce函数的示例。

4. 使用视图和查询

4.1 组织视图

每个存储桶会将视图组织成一个或多个设计文档,理论上,每个设计文档的视图数量没有限制。但是,为了获得最佳性能,建议你将每个设计文档的视图数量限制在10个以内。

当你首次在设计文档中创建视图时,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,可以遍历行来获取文档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
}

我们将这些文档存储在“tuyucheng-tutorial”存储桶中,并将所有视图存储在名为“studentGrades”的设计文档中;让我们看一下打开存储桶以便查询它所需的代码:

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

6. 精确匹配查询

假设你想查找某一门或一组课程的所有学生成绩,让我们使用以下map函数编写一个名为“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字段的视图,让我们为“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”,因此必须包含一个最高可能成绩值的上限。否则,我们的结果集将包含所有课程值按字典顺序大于“Math”的文档。

查找所有不及格的数学成绩(低于60分):

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

与前面的例子类似,我们必须指定一个最低可能的成绩下限。否则,我们的结果集将包含所有course值按字典顺序小于“Math”的成绩。

最后,为了找到数学成绩最高的5个(除非有平分),你可以告诉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”函数时,我们必须为每个条目发出要求和的值-在本例中为积分数。

让我们编写一个查询来查找每个学生的总学分数:

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

9. 总结

我们演示了如何在Couchbase中编写一些基本的MapReduce视图,以及如何构建和执行针对视图的查询并提取结果。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章