Interrogation de Couchbase avec MapReduce Views

1. Vue d’ensemble

Dans ce tutoriel, nous allons présenter quelques vues MapReduce simples et expliquer comment les interroger à l’aide du Couchbase .

2. Dépendance Maven

Pour utiliser Couchbase dans un projet Maven, importez le SDK de Couchbase dans votre pom.xml :

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

Vous pouvez trouver la dernière version sur Maven Central .

3. MapReduce Views

Dans Couchbase, une vue MapReduce est un type d’index qui peut être utilisé pour interroger un compartiment de données. Il est défini à l’aide d’une fonction JavaScript map et d’une fonction facultative reduce .

3.1. La fonction map

La fonction map est exécutée une fois sur chaque document. Lorsque la vue est créée, la fonction map est exécutée une fois sur chaque document du compartiment et les résultats sont stockés dans le compartiment.

Une fois la vue créée, la fonction map est exécutée uniquement sur les documents récemment insérés ou mis à jour afin de mettre à jour la vue de manière incrémentielle.

Les résultats de la fonction map étant stockés dans le compartiment de données, les requêtes sur une vue présentent des latences faibles.

Voyons un exemple de fonction map qui crée un index sur le champ name de tous les documents du compartiment dont le champ type est égal à «StudentGrade» :

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

La fonction emit indique à Couchbase quel (s) champ (s) de données à stocker dans la clé d’index (premier paramètre) et quelle valeur (deuxième paramètre) à associer au document indexé.

Dans ce cas, nous ne stockons que la propriété document name dans la clé d’index. Et comme nous ne souhaitons pas associer une valeur particulière à chaque entrée, nous passons null comme paramètre value.

Lors du traitement de la vue, Couchbase crée un index des clés émises par la fonction map , en associant chaque clé à tous les documents pour lesquels cette clé a été émise.

Par exemple, si trois propriétés ont la propriété name définie sur «John Doe» , la clé d’indexation «John Doe» sera associée à ces trois documents.

3.2. La fonction reduce

La fonction reduce est utilisée pour effectuer des calculs d’agrégat à l’aide des résultats d’une fonction map . L’interface utilisateur administrative de Couchbase offre un moyen simple d’appliquer les fonctions reduce intégrées «nombre », « sum», et « stats» à votre fonction map__.

Vous pouvez également écrire vos propres fonctions reduce pour des agrégations plus complexes. Nous verrons des exemples d’utilisation des fonctions intégrées reduce plus loin dans ce didacticiel.

4. Travailler avec des vues et des requêtes

4.1. Organiser les vues

Les vues sont organisées en un ou plusieurs documents de conception par compartiment. En théorie, le nombre de vues par document de conception est illimité.

Cependant, pour des performances optimales, il a été suggéré de limiter chaque document de conception à moins de dix vues.

Lorsque vous créez une vue dans un document de conception, Couchbase la désigne comme une vue development . Vous pouvez exécuter des requêtes sur une vue development pour tester ses fonctionnalités. Une fois que vous êtes satisfait de la vue, vous publiez le document de conception, et la vue devient une vue production .

4.2. Construire des requêtes

Pour construire une requête sur une vue Couchbase, vous devez fournir son nom de document de conception et son nom de vue afin de créer un objet ViewQuery :

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

Une fois exécutée, cette requête retournera toutes les lignes de la vue. Nous verrons dans les sections suivantes comment restreindre le jeu de résultats en fonction des valeurs de clé.

Pour construire une requête sur une vue de développement, vous pouvez appliquer la méthode development () lors de la création de la requête:

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

4.3. Exécuter la requête

Une fois que nous avons un objet ViewQuery , nous pouvons exécuter la requête pour obtenir un ViewResult :

ViewResult result = bucket.query(query);

4.4. Traitement des résultats de la requête

Et maintenant que nous avons ViewResult , nous pouvons parcourir les lignes pour obtenir les identifiants et/ou le contenu du document:

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

5. Exemple d’application

Pour le reste du didacticiel, nous écrirons des vues et des requêtes MapReduce pour un ensemble de documents relatifs aux notes des étudiants ayant le format suivant, les notes étant comprises entre 0 et 100:

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

Nous allons stocker ces documents dans le compartiment « baeldung-tutorial » et toutes les vues dans un document de conception intitulé « studentGrades ». Voyons le code nécessaire pour ouvrir le compartiment afin de pouvoir l’interroger:

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

6. Requêtes de correspondance exacte

Supposons que vous souhaitiez trouver toutes les notes des étudiants pour un cours ou un ensemble de cours particulier. Écrivons une vue appelée « findByCourse » à l’aide de la fonction map suivante:

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

Notez que dans cette vue simple, il suffit d’émettre le champ course .

6.1. Correspondance sur une seule clé

Pour trouver toutes les notes du cours d’histoire, nous appliquons la méthode key à notre requête de base:

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

6.2. Correspondance sur plusieurs touches

Si vous souhaitez rechercher toutes les notes des cours de mathématiques et de sciences, vous pouvez appliquer la méthode keys à la requête de base en lui transmettant un tableau de valeurs de clé:

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

7. Requêtes de plage

Pour pouvoir rechercher des documents contenant une plage de valeurs pour un ou plusieurs champs, nous avons besoin d’une vue qui émet le (s) champ (s) qui nous intéresse (s) et nous devons spécifier une limite inférieure et/ou supérieure pour la requête.

Voyons maintenant comment effectuer des requêtes de plage impliquant un seul champ et plusieurs champs.

7.1. Requêtes impliquant un seul champ

Pour rechercher tous les documents avec une plage de valeurs grade , quelle que soit la valeur du champ course , nous avons besoin d’une vue qui n’émet que le champ grade . Écrivons la fonction map pour la vue « findByGrade »:

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

Écrivons une requête en Java en utilisant cette vue pour trouver toutes les notes équivalentes à une lettre «B» (de 80 à 89 inclus):

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

Notez que la valeur de la clé de début dans une requête de plage est toujours traitée comme inclusive.

Et si toutes les notes sont connues pour être des entiers, la requête suivante donnera les mêmes résultats:

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

Pour trouver tous les grades «A» (90 et plus), il suffit de spécifier la limite inférieure:

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

Et pour trouver toutes les notes d’échec (inférieures à 60), il suffit de spécifier la limite supérieure:

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

7.2. Requêtes impliquant plusieurs champs

Supposons maintenant que nous voulions trouver tous les étudiants d’un cours spécifique dont les notes se situent dans une certaine plage. Cette requête nécessite une nouvelle vue qui émet les champs course et grade .

Avec les vues multi-champs, chaque clé d’index est émise sous forme de tableau de valeurs.

Comme notre requête implique une valeur fixe pour course et une plage de valeurs grade , nous écrirons la fonction map pour émettre chaque clé sous forme de tableau de la forme[ course , grade ].

Regardons la fonction map pour la vue “ findByCourseAndGrade “:

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

Lorsque cette vue est renseignée dans Couchbase, les entrées d’index sont triées par course et grade . Voici un sous-ensemble de clés dans la vue « findByCourseAndGrade », présentées dans leur ordre de tri naturel:

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

Étant donné que les clés de cette vue sont des tableaux, vous utiliseriez également des tableaux de ce format pour spécifier les limites inférieure et supérieure d’une requête d’intervalle par rapport à cette vue.

Cela signifie que pour trouver tous les étudiants ayant obtenu une note «B» (80 à 89) dans le cours de mathématiques, vous devez définir la limite inférieure sur:

----["Math", 80]----

et la borne supérieure à:

----["Math", 89]----

Écrivons la requête de plage en Java:

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

Si nous voulons trouver pour tous les étudiants qui ont reçu une note «A» (90 et plus) en mathématiques, alors nous écririons:

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

Notez que parce que nous fixons la valeur du cours à « Math », nous devons inclure une limite supérieure avec la valeur grade la plus élevée possible.

Sinon, notre ensemble de résultats inclurait également tous les documents dont la valeur de course est lexicographiquement supérieure à « Math ».

Et pour trouver tous les échecs en mathématiques (moins de 60):

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

Comme dans l’exemple précédent, nous devons spécifier une limite inférieure avec la note la plus basse possible. Sinon, notre ensemble de résultats inclurait également toutes les notes où la valeur de course est lexicographiquement inférieure à «Math **

Enfin, pour trouver les cinq notes mathématiques les plus élevées (sauf les égalités), vous pouvez demander à Couchbase d’effectuer un tri décroissant et de limiter la taille de l’ensemble de résultats:

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

Notez que lorsque vous effectuez un tri décroissant, les valeurs startKey et endKey sont inversées, car Couchbase applique le tri avant d’appliquer le limit .

8. Requêtes agrégées

L’un des points forts des vues MapReduce est qu’elles sont extrêmement efficaces pour l’exécution de requêtes agrégées sur des ensembles de données volumineux. Dans notre ensemble de données sur les notes des élèves, par exemple, nous pouvons facilement calculer les agrégats suivants:

  • nombre d’étudiants dans chaque cours

  • somme des heures de crédit pour chaque étudiant

  • Moyenne pondérée pour chaque élève dans tous les cours

Construisons une vue et une requête pour chacun de ces calculs en utilisant les fonctions intégrées reduce .

** 8.1. Utilisation de la fonction count ()

Commençons par écrire la fonction map pour qu’une vue compte le nombre d’étudiants dans chaque cours:

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

Nous appellerons cette vue « countStudentsByCourse » et indiquerons qu’elle utilisera la fonction intégrée « count» . Et comme nous n’effectuons qu’un simple compte, nous pouvons toujours émettre null comme valeur pour chaque entrée.

Pour compter le nombre d’étudiants dans chaque cours:

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

L’extraction de données à partir de requêtes agrégées diffère de ce que nous avons vu jusqu’à présent. Au lieu d’extraire un document Couchbase correspondant pour chaque ligne du résultat, nous extrayons les clés d’agrégation et les résultats.

Exécutons la requête et extrayons les comptes dans un fichier 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. Utilisation de la fonction sum ()

Ensuite, écrivons une vue calculant la somme des heures de crédit tentées par chaque élève. Nous appellerons cette vue « sumHoursByStudent » et indiquerons qu’elle utilisera la fonction intégrée « sum» __:

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

Notez que lors de l’application de la fonction sum” , nous devons émettre__ la valeur à additionner - dans ce cas, le nombre de crédits - pour chaque entrée.

Ecrivez une requête pour trouver le nombre total de crédits pour chaque étudiant:

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

Et maintenant, exécutons la requête et extrayons les sommes agrégées dans un fichier 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. Calcul des moyennes des notes

Supposons que nous voulions calculer la moyenne pondérée cumulative de chaque élève pour tous les cours, en utilisant l’échelle conventionnelle en fonction des notes obtenues et du nombre d’heures de crédit valorisées par le cours (A = 4 points par heure d’heures de crédit, B = 3 points par heure de crédit, C = 2 points par heure de crédit et D = 1 point par heure de crédit).

Il n’existe pas de fonction intégrée reduce permettant de calculer les valeurs moyennes. Nous allons donc combiner les résultats de deux vues pour calculer le GPA.

Nous avons déjà la vue «sumHoursByStudent» qui résume le nombre d’heures-crédits que chaque étudiant a tenté. Nous avons maintenant besoin du nombre total de points obtenus par chaque élève.

Créons une vue appelée «sumGradePointsByStudent», qui calcule le nombre de points obtenus pour chaque cours suivi. Nous allons utiliser la fonction intégrée sum” pour réduire la fonction map__ suivante:

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

Examinons maintenant cette vue et extrayons les sommes dans un fichier 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);
}

Enfin, combinons les deux _Map _ afin de calculer la moyenne cumulative pour chaque élève:

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

Nous avons montré comment écrire des vues MapReduce de base dans Couchbase et comment construire et exécuter des requêtes sur les vues et extraire les résultats.

Le code présenté dans ce tutoriel se trouve dans le projet GitHub

Vous pouvez en savoir plus sur les MapReduce views et comment query les en Java à la page officielle Site de documentation pour développeurs Couchbase .