Consultando o Couchbase com exibições MapReduce

Consultando o Couchbase com exibições MapReduce

*1. Visão geral *

Neste tutorial, apresentaremos algumas visualizações simples do MapReduce e demonstraremos como consultá-las usando o SDK Java doouchbase

===* 2. Dependência Maven *

Para trabalhar com o Couchbase em um projeto Maven, importe o Couchbase SDK para o seu pom.xml:

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

===* 3. Visualizações MapReduce *

No Couchbase, uma visualização do MapReduce é um tipo de índice que pode ser usado para consultar um intervalo de dados. É definido usando uma função map JavaScript e uma função reduce opcional.

====* 3.1 A função map *

A função map é executada em cada documento uma vez. Quando a exibição é criada, a função map é executada uma vez em cada documento no bucket e os resultados são armazenados no bucket.

Depois que uma exibição é criada, a função map é executada apenas em documentos recém-inseridos ou atualizados, a fim de atualizar a exibição incrementalmente.

Como os resultados da função map são armazenados no intervalo de dados, as consultas em uma exibição exibem latências baixas.

Vejamos um exemplo de uma função map que cria um índice no campo name de todos os documentos no bucket cujo campo type é igual a _ "StudentGrade" _:

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

A função emit informa ao Couchbase quais campos de dados armazenar na chave de índice (primeiro parâmetro) e qual valor (segundo parâmetro) a ser associado ao documento indexado.

Nesse caso, estamos armazenando apenas a propriedade name do documento na chave de índice. E como não estamos interessados ​​em associar nenhum valor específico a cada entrada, passamos null como o parâmetro value.

À medida que o Couchbase processa a exibição, ele cria um índice das chaves emitidas pela função map, associando cada chave a todos os documentos para os quais essa chave foi emitida.

Por exemplo, se três documentos tiverem a propriedade name definida como _ “John Doe” _, a chave de índice _ “John Doe” _ estará associada a esses três documentos.

====* 3.2 A função reduce *

A função reduce é usada para executar cálculos agregados usando os resultados de uma função map. A interface do usuário do Couchbase Admin fornece uma maneira fácil de aplicar as funções reduce internas _ “_ count”, “sum”, _ e _ “ stats” , à sua função _map.

Você também pode escrever suas próprias funções reduce para agregações mais complexas. Veremos exemplos de uso das funções internas reduce posteriormente no tutorial.

===* 4. Trabalhando com vistas e consultas *

====* 4.1 Organizando as vistas *

As vistas são organizadas em um ou mais documentos de design por bucket. Em teoria, não há limite para o número de visualizações por documento de design. No entanto, para obter o desempenho ideal, sugerimos que você limite cada documento de design a menos de dez visualizações.

Quando você cria uma exibição pela primeira vez em um documento de design, o Couchbase a designa como uma exibição development. Você pode executar consultas em uma visualização development para testar sua funcionalidade. Quando estiver satisfeito com a exibição, você poderá publicar o documento de design e a exibição se tornará uma exibição de produção.

====* 4.2 Construindo consultas *

Para construir uma consulta em uma exibição do Couchbase, é necessário fornecer o nome do documento de design e o nome da exibição para criar um objeto ViewQuery:

ViewQuery query = ViewQuery.from("design-document-name", "view-name");

Quando executada, essa consulta retornará todas as linhas da exibição. Veremos nas seções posteriores como restringir o conjunto de resultados com base nos valores-chave.

Para construir uma consulta em uma exibição de desenvolvimento, você pode aplicar o método _development () _ ao criar a consulta:

ViewQuery query
  = ViewQuery.from("design-doc-name", "view-name").development();

====* 4.3 Executando a consulta *

Depois de termos um objeto ViewQuery, podemos executar a consulta para obter um ViewResult:

ViewResult result = bucket.query(query);

====* 4.4 Processando resultados da consulta *

E agora que temos um ViewResult, podemos percorrer as linhas para obter os IDs e/ou o conteúdo do documento:

for(ViewRow row : result.allRows()) {
    JsonDocument doc = row.document();
    String id = doc.id();
    String json = doc.content().toString();
}

===* 5. Aplicativo de amostra *

No restante do tutorial, escreveremos visualizações e consultas do MapReduce para um conjunto de documentos de notas de alunos com o seguinte formato, com notas restritas ao intervalo de 0 a 100:

{
    "type": "StudentGrade",
    "name": "John Doe",
    "course": "History",
    "hours": 3,
    "grade": 95
}

Armazenaremos esses documentos no intervalo "-tutorial" e todas as visualizações em um documento de design chamado "studentGrades". Vejamos o código necessário para abrir o bucket, para que possamos consultá-lo:

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

===* 6. Consultas de correspondência exata *

Suponha que você queira encontrar todas as notas dos alunos para um curso ou conjunto de cursos específico. Vamos escrever uma visão chamada “findByCourse” usando a seguinte função map:

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

Observe que, nesta visão simples, precisamos apenas emitir o campo course.

====* 6.1 Correspondência em uma única chave *

Para encontrar todas as notas do curso de História, aplicamos o método key à nossa consulta básica:

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

====* 6.2 Correspondência em várias chaves *

Se você deseja encontrar todas as notas dos cursos de Matemática e Ciências, pode aplicar o método keys à consulta base, passando uma matriz de valores-chave:

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

===* 7. Consultas por intervalo *

Para consultar documentos contendo um intervalo de valores para um ou mais campos, precisamos de uma exibição que emita o (s) campo (s) em que estamos interessados ​​e precisamos especificar um limite inferior e/ou superior para a consulta.

Vamos dar uma olhada em como executar consultas de intervalo envolvendo um único campo e vários campos.

====* 7.1 Consultas envolvendo um único campo *

Para localizar todos os documentos com um intervalo de valores grade, independentemente do valor do campo course, precisamos de uma exibição que emita apenas o campo grade. Vamos escrever a função map para a visualização "findByGrade":

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

Vamos escrever uma consulta em Java usando essa visualização para encontrar todas as notas equivalentes a uma nota de letra “B” (80 a 89 inclusive):

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

Observe que o valor da chave inicial em uma consulta de intervalo é sempre tratado como inclusivo.

E se todas as notas forem conhecidas como inteiras, a consulta a seguir produzirá os mesmos resultados:

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

Para encontrar todas as notas "A" (90 e acima), precisamos apenas especificar o limite inferior:

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

E para encontrar todas as notas reprovadas (abaixo de 60), precisamos apenas especificar o limite superior:

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

====* 7.2 Consultas envolvendo vários campos *

Agora, suponha que desejamos encontrar todos os alunos em um curso específico cuja nota caia em um determinado intervalo. Esta consulta requer uma nova visualização que emita os campos course e grade.

Com visualizações de vários campos, cada chave de índice é emitida como uma matriz de valores. Como nossa consulta envolve um valor fixo para course e um intervalo de valores de grade, escreveremos a função map para emitir cada tecla como uma matriz do formato [course, grade].

Vamos dar uma olhada na função map para a exibição "findByCourseAndGrade":

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

Quando essa visualização é preenchida no Couchbase, as entradas do índice são classificadas por course e grade. Aqui está um subconjunto de chaves na exibição "findByCourseAndGrade" mostrada em sua ordem de classificação natural:

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

Como as chaves nesta exibição são matrizes, você também usaria matrizes desse formato ao especificar os limites inferior e superior de uma consulta de intervalo nessa exibição.

Isso significa que, para encontrar todos os alunos que obtiveram uma nota “B” (80 a 89) no curso de Matemática, defina o limite inferior para:

["Math", 80]

e o limite superior para:

["Math", 89]

Vamos escrever a consulta de intervalo em Java:

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

Se quisermos encontrar para todos os alunos que receberam uma nota “A” (90 e acima) em Matemática, escreveríamos:

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

Observe que, como estamos fixando o valor do curso como "Math", precisamos incluir um limite superior com o maior valor possível de grade. Caso contrário, nosso conjunto de resultados também incluiria todos os documentos cujo valor de course é lexicograficamente maior que "Math".

E para encontrar todas as notas de matemática com falha (abaixo de 60):

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

Assim como no exemplo anterior, devemos especificar um limite inferior com a nota mais baixa possível. Caso contrário, nosso conjunto de resultados também incluiria todas as notas em que o valor de course é lexicograficamente menor que "Math".

Por fim, para encontrar as cinco notas mais altas de Matemática (com exceção de qualquer empate), você pode pedir ao Couchbase para realizar uma classificação decrescente e limitar o tamanho do conjunto de resultados:

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

Observe que, ao executar uma classificação decrescente, os valores startKey e endKey são revertidos, porque o Couchbase aplica a classificação antes de aplicar o limit.

===* 8. Consultas agregadas *

Um dos pontos fortes das visualizações do MapReduce é que elas são altamente eficientes para executar consultas agregadas em grandes conjuntos de dados. No conjunto de dados de notas dos alunos, por exemplo, podemos calcular facilmente os seguintes agregados:

  • número de alunos em cada curso

  • soma das horas de crédito para cada aluno *nota média para cada aluno em todos os cursos

Vamos criar uma visualização e consulta para cada um desses cálculos usando as funções reduce internas.

8.1. Usando a função _count () _*

Primeiro, vamos escrever a função map para uma visualização para contar o número de alunos em cada curso:

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

Chamaremos essa visualização de "countStudentsByCourse" e designaremos que ela deve usar a função interna _ "_ count" . E como estamos executando apenas uma contagem simples, ainda podemos emitir _null como o valor de cada entrada.

Para contar o número de alunos em cada curso:

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

A extração de dados de consultas agregadas é diferente do que vimos até o momento. Em vez de extrair um documento do Couchbase correspondente para cada linha do resultado, estamos extraindo as chaves e os resultados agregados.

Vamos executar a consulta e extrair as contagens em um 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. Usando a função _sum () _ *

Em seguida, vamos escrever uma exibição que calcula a soma das horas de crédito de cada aluno tentadas. Chamaremos essa visualização de "sumHoursByStudent" e designaremos que ela deve usar a função embutida _ "_ sum" _:

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

Observe que, ao aplicar a função _ “_ sum” , temos que _emit o valor a ser somado - nesse caso, o número de créditos - para cada entrada.

Vamos escrever uma consulta para encontrar o número total de créditos para cada aluno:

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

E agora, vamos executar a consulta e extrair as somas agregadas em um 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. Cálculo das médias de pontos de classificação *

Suponha que desejemos calcular a média de notas de cada aluno (GPA) em todos os cursos, usando a escala convencional de notas com base nas notas obtidas e no número de horas de crédito que o curso vale (A = 4 pontos por hora de crédito, B = 3 pontos por hora de crédito, C = 2 pontos por hora de crédito e D = 1 ponto por hora de crédito).

Não há função reduce integrada para calcular valores médios, portanto, combinaremos a saída de duas visualizações para calcular o GPA.

Já temos a exibição _ "sumHoursByStudent" _ que soma o número de horas de crédito que cada aluno tentou. Agora precisamos do número total de notas que cada aluno obteve.

Vamos criar uma exibição chamada _ “sumGradePointsByStudent” _ que calcula o número de notas obtidas em cada curso realizado. Usaremos a função _ _ _ sum _ embutida para reduzir a seguinte função 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);
        }
    }
}

Agora vamos consultar essa visualização e extrair as somas em um 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);
}

Por fim, vamos combinar os dois Maps para calcular o GPA para cada aluno:

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. Conclusão

Demonstramos como escrever algumas visualizações básicas do MapReduce no Couchbase, e como construir e executar consultas nas visualizações e extrair os resultados.

O código apresentado neste tutorial pode ser encontrado no GitHub project.

Você pode saber mais sobre MapReduce views e como query-los em Java no https://developer.couchbase.com/documentation/server/current/sdk/development-intro.html oficial [Site de documentação do desenvolvedor do Couchbase].