Gestion des documents de conception

Le magasin de données JSON évolutif pour IBM Cloudant possède plusieurs mécanismes d'interrogation, chacun générant des index qui sont créés et conservés séparément depuis les données coeur.

Article signé Glynn Bird, chargé du pôle de développement pour IBM Cloudant, glynn@cloudant.com.

L'indexation n'est pas réalisée immédiatement lorsqu'un document est sauvegardé. Son exécution est prévue ultérieurement afin d'offrir un débit d'écriture non-bloquant plus rapide.

  • Les vues MapReduce sont des index dans l'ensemble de données avec des paires clé-valeur qui sont stockées dans un arbre B afin d'être plus rapidement extraites par la clé ou la plage de clés.
  • Les index de recherche sont construits à l'aide d'Apache Lucene pour permettre la recherche de texte libre, la création de facettes et les requêtes ad hoc complexes.

Les index de recherche et les vues MapReduce d'IBM® Cloudant® for IBM Cloud® sont configurés en ajoutant des documents de conception à une base de données. Les documents de conception sont des documents JSON qui contiennent des instructions sur la façon dont la vue ou l'index doivent être créés. Prenons un exemple simple. Supposons que vous disposez d'une simple collection de documents de données, comme dans l'exemple suivant.

Voici un exemple de document de données simple :

{
    "_id": "23966717-5A6F-E581-AF79-BB55D6BBB613",
    "_rev": "1-96daf2e7c7c0c277d0a63c49b57919bc",
    "doc_name": "Markdown Reference",
    "body": "Lorem Ipsum",
    "ts": 1422358827
}

Chaque document de données comporte un nom, un corps de texte et un horodatage. Vous créez une vue MapReduce pour trier vos documents par horodatage.

Vous pouvez trier vos documents par horodatage en créant une fonction de mappe.

Voici un exemple de fonction de mappe qui renvoie la zone d'horodatage d'un document, le cas échéant :

function(doc) {
    if (doc.ts) {
        emit( doc.ts, null);
    }
}

La fonction émet l'horodatage du document pour que vous puissiez l'utiliser comme clé d'index. Puisque nous ne sommes pas intéressés par la valeur de l'index, null est émis. L'objectif est de fournir un index ordonné chronologiquement dans l'ensemble du document.

Nous allons appeler cette vue by_ts et la placer dans un document de conception appelé fetch.

Voici un exemple de document de conception qui définit une vue à l'aide d'une fonction de mappe :

{
    "_id": "_design/fetch",
    "views": {
      "by_ts": {
        "map": "function(doc) {
          if (doc.ts) {
            emit( doc.ts, null);
          }
        }"
      }
    },
    "language": "javascript"
}

Il en résulte que le code de mappage est transformé en chaîne compatible JSON, puis inclus dans un document de conception.

Une fois le document de conception enregistré, IBM Cloudant déclenche des processus côté serveur pour générer la vue fetch/by_ts. Pour créer la vue, il procède à l'itération de chaque document dans la base de données, puis il envoie chacun d'eux vers la fonction de mappe JavaScript. La fonction renvoie la paire key-value émise. A mesure que l'itération se poursuit, chaque paire key-value est stockée dans un index B-Tree. Lorsque l'index est généré pour la première fois, la réindexation suivante s'applique uniquement aux documents nouveaux et mis à jour. Les documents supprimés ne sont plus indexés. Ce processus rapide est appelé Incremental MapReduce, tel qu'illustré dans le diagramme ci-dessous :

Une base de données est créée avec 1000 documents. Un document de conception est ajouté, ce qui déclenche la construction d'une ou plusieurs vues. La vue se construit de manière asynchrone jusqu'à ce qu'elle soit complète. L'arrivée de 250 documents supplémentaires rend à nouveau l'index incomplet. La vue est générée automatiquement en arrière-plan ou lorsqu'elle est interrogée.
Illustration de l'incrémentation MapReduce

Il convient de rappeler les points suivants :

  • La construction d'un index est asynchrone. IBM Cloudant confirme que le document de conception a été sauvegardé. Pour vérifier la progression de la construction de l'index, vous devez interroger le noeud final _active_tasks d'IBM Cloudant.
  • Plus votre quantité de données est importante, plus il faut de temps avant que l'index ne soit prêt.
  • Pendant le procédé de génération d'index initial, toutes les requêtes sur cet index seront bloquées.
  • L'interrogation d'une vue déclenche le "mappage" des documents qui ne sont pas indexés de manière incrémentielle. Cette pratique garantit l'obtention d'une vue actualisée des données. Consultez la discussion suivante relative au paramètre stale pour en savoir plus sur les exceptions à cette règle.

Vues multiples dans un même document de conception

Si vous définissez plusieurs vues dans un même document de conception, celles-ci sont générées de manière efficace et simultanée. Chaque document est lu une seule fois, avant d'être soumis à la fonction de mappe de la vue. Si vous utilisez cette approche, gardez à l'esprit que la modification d'un document de conception invalide toutes les vues MapReduce existantes définies dans le document. Ce processus invalide les vues MapReduce même si certaines vues ne sont pas altérées.

Si les vues MapReduce doivent être modifiées indépendamment les unes des autres, placez leurs définitions dans des documents de conception séparés.

Ce comportement ne s'applique pas aux index de recherche Lucene. Ces derniers peuvent être modifiés dans le même document de conception sans invalider d'autres index qui, eux, n'ont pas été modifiés au sein du même document.

Une base de données est créée avec 1000 documents. Un document de conception est ajouté pour déclencher la génération de 2 vues et de 2 index de recherche. La vue se construit de manière asynchrone jusqu'à ce qu'elle soit complète. L'arrivée d'une deuxième version du document de conception invalide toutes les vues MapReduce et tous les index de recherche modifiés dans le document.
Modification de la version du document de conception

Gestion des changements dans un document de conception

Imaginons que vous décidiez plus tard de modifier la conception de la vue. A présent, au lieu de retourner le résultat de l'horodatage réel, nous allons nous intéresser exclusivement au nombre de documents remplissant les critères. Pour obtenir ce nombre, la fonction de mappe est inchangée, mais vous utilisez maintenant un paramètre reduce ayant pour valeur_count.

Voici un exemple de document de conception qui utilise une fonction de réduction :

{
    "_id": "_design/fetch",
    "_rev": "2-a2324c9e74a76d2a16179c56f5315dba",
    "views": {
        "by_ts": {
            "map": "function(doc) {
                if (doc.ts) {
                  emit( doc.ts, null);
                }
            }
        }",
        "reduce": "_count"
    },
    "language": "javascript"
}

Lorsque ce document de conception est enregistré, IBM Cloudant invalide complètement l'ancien index et commence à construire le nouvel index à partir de zéro, en répétant l'opération sur chaque document un par un. Comme pour la génération initiale, le délai dépend du nombre de documents dans la base de données. Les requêtes entrantes sont également bloquées sur cette vue par la génération tant que celle-ci n'est pas terminée.

Mais il y a un hic...

Si une application accède à cette vue en temps réel, il se peut que vous soyez face à un dilemme en matière de déploiement :

  • La version 1 du code, qui s'appuie sur le document de conception original, risque de ne plus fonctionner car l'ancienne vue a été invalidée.
  • La version 2 du code utilise le nouveau document de conception. Cette version ne peut pas être publiée immédiatement car la génération de la nouvelle vue n'est pas encore terminée. N'oubliez pas que le processus de génération prend plus de temps si la base de données contient de nombreux documents.
  • Un problème plus subtil qui affecte le code est que les versions 1 et 2 attendent des données de résultat différentes de la vue: La version 1 attend une liste de documents correspondants, tandis que la version 2 prévoit une " réduction " du nombre de résultats.

Coordination des changements dans les documents de conception

Vous pouvez traiter ce problème de contrôle des changements de deux manières.

Documents de conception avec version

Une solution consiste à utiliser des noms de documents de conception avec version :

  • Le code est initialement écrit pour utiliser une vue nommée _design/fetchv1.
  • Lorsque vous publiez une nouvelle version, vous créez une vue appelée _design/fetchv2, puis vous interrogez cette vue pour vérifier que sa génération est en cours.
  • IBM Cloudant interroge _active_tasks jusqu'à ce que le travail de création du nouvel index soit terminé.
  • Vous êtes maintenant prêt à publier le code qui dépend de la seconde vue.
  • Supprimez _design/fetchv1 lorsque vous êtes certain de ne plus en avoir besoin.

L'utilisation de documents de conception avec version constitue une manière simple de gérer le contrôle des changements dans vos documents de conception, mais vous devez penser à supprimer les anciennes versions ultérieurement.

Move and switchDocuments de conception

Une autre approche repose sur le fait qu'IBM Cloudant reconnaît quand deux documents de conception sont identiques et qu'il ne perd donc pas inutilement du temps et des ressources à reconstruire des vues qu'il possède déjà. En d'autres termes, si vous prenez votre document de conception _design/fetch et que vous en créez une réplique exacte nommée _design/fetch_OLD, les deux noeuds finaux fonctionnent de manière interchangeable sans déclencher de réindexation.

Pour passer à la nouvelle vue, procédez comme suit :

  1. Créez une copie en double du document de conception que vous souhaitez modifier, par exemple en ajoutant _OLD à son nom : _design/fetch_OLD.
  2. Placez le document de conception (nouveau ou "entrant") dans la base de données en utilisant un nom avec le suffixe _NEW : _design/fetch_NEW.
  3. Interrogez la vue fetch_NEW pour vous assurer que sa génération démarre.
  4. Interrogez le noeud final _active_tasks et patientez jusqu'à la fin de la génération d'index.
  5. Placez une copie du nouveau document de conception dans _design/fetch.
  6. Supprimez le document de conception _design/fetch_NEW.
  7. Supprimez le document de conception _design/fetch_OLD.

Move and switch outils

Le script couchmigrate de la ligne de commande Node.js automatise la procédure de Move and switch. Pour l'installer, utilisez la commande suivante :

npm install -g couchmigrate

Pour utiliser le script couchmigrate, définissez d'abord l'instance URL de CouchDB/{{site. data.keyword.cloudant_short_notm }} en définissant une variable d'environnement appelée COUCH_URL. Exécutez la commande suivante afin de définir l'URL pour l'instance IBM Cloudant :

export COUCH_URL=https://127.0.0.1:5984

Le site URL doit commencer par https:// et peut inclure des informations d'authentification. Exécutez la commande suivante afin de définir l'URL de l'instance IBM Cloudant avec des données d'authentification :

export COUCH_URL="https://$ACCOUNT:$PASSWORD@$HOST.cloudant.com"

En admettant que vous disposiez d'un document de conception au format JSON stocké dans un fichier, vous pouvez ensuite exécuter la commande de migration.

Dans cet exemple, db spécifie le nom de la base de données à modifier, et dd indique le chemin d'accès au fichier du document de conception. Exécutez la commande couchmigrate :

couchmigrate --db mydb --dd /path/to/my/dd.json

Le script coordonne la procédure Move and switch, attendant que la vue soit générée avant de retourner à son point d'origine. Si le document de conception entrant est identique à celui d'origine, le script est retourné presque immédiatement.

Le code source du script est disponible ici : couchmigrate.

Paramètre 'stale'

Si un index est terminé, mais que de nouveaux enregistrements sont ajoutés à la base de données, la mise à jour de l'index en arrière-plan est planifiée. L'état de la base de données est illustré dans le diagramme suivant :

La vue est complète. L'arrivée de 250 documents supplémentaires rend à nouveau l'index incomplet.
Mise à jour de l'index prévue

Lorsque vous interrogez la vue, trois choix sont possibles :

  • Le comportement par défaut consiste à vérifier que l'index est à jour, et que la base de données contient les documents les plus récents, avant que la réponse ne soit renvoyée. Lorsque vous interrogez la vue, IBM Cloudant indexe d'abord les 250 nouveaux documents, puis renvoie la réponse.
  • Une deuxième solution consiste à ajouter le paramètre stale=ok à l'appel API. Ce paramètre signifie, return me the data that is already indexed. I don't care about the latest updates. En d'autres termes, lorsque vous interrogez la vue avec stale=ok, IBM Cloudant renvoie la réponse immédiatement, sans réindexation supplémentaire.
  • Une troisième solution consiste à ajouter le paramètre stale=update_after à l'appel API. Ce paramètre signifie, return me the data that is already indexed, and then reindex any new documents. En d'autres termes, lorsque vous interrogez la vue avec stale=update_after, IBM Cloudant renvoie la réponse immédiatement, puis planifie une tâche d'arrière-plan pour indexer les nouvelles données.

L'ajout de stale=ok ou stale=update_after peut être un bon moyen d'obtenir une réponse rapide de la vue, mais au détriment de l'obtention de données actualisées.

Le comportement par défaut répartit équitablement la charge entre les noeuds du cluster d'IBM Cloudant. Si vous utilisez les options stale=ok oustale=update_after alternatives, celles-ci peuvent favoriser un sous-ensemble de noeuds du cluster, afin de renvoyer des résultats cohérents depuis l'ensemble cohérent à terme. Le paramètre stale n'est pas la solution parfaite pour tous les cas d'utilisation. Il peut toutefois permettre d'obtenir des réponses rapides sur des ensembles de données en perpétuel changement, si votre application accepte des résultats périmés. Si le taux de changement de vos données est faible, l'ajout de stale=ok ou de stale=update_after ne présente aucun avantage en termes de performances, et risque même de répartir inégalement la charge sur des clusters plus importants.

Evitez le paramètre stale=ok ou stale=update_after dans la mesure du possible car le comportement par défaut fournit les données les plus récentes et répartit les données dans le cluster. Vous pouvez indiquer à une application client qu'une longue tâche de traitement de données est en cours d'exécution (par exemple pendant une mise à jour de données non formatées) en basculant temporairement vers le paramètre stale=ok pendant ces périodes. L'application peut revenir au comportement par défaut par la suite.

L'option stale est toujours disponible, mais les options les plus utiles, stable et update, sont également disponibles et leur utilisation doit être privilégiée. Pour plus d'informations, voir Accessing a stale view.