Recettes

Ce chapitre explore quelques problèmes courants et aborde leur résolution avec CouchDB, le tout en s’appuyant sur les bonnes pratiques et des instructions simples à suivre, pas après pas.

La Banque

Banquier, c’est un métier sérieux, qui nécessite des bases de données tout aussi sérieuses pour stocker des transactions sérieuses et des informations sérieuses relatives aux comptes. Ils ne peuvent pas laisser l’argent s’évaporer. Jamais. Pas plus qu’ils ne peuvent créer de l’argent. Une banque doit être équilibrée, en tout temps.

La sagesse populaire dit qu’une base de données doit implémenter les transactions pour être prise au sérieux. CouchDB ne supporte pas les transactions dans le sens traditionnel du terme (bien qu’il fonctionne de manière transactionnelle), aussi pourriez-vous conclure que couchDB n’est pas adapté pour stocker des données bancaires. En outre, qui confierait son argent à un canapé ? Eh bien, nous, nous lui ferions confiance. Ce chapitre explique pourquoi.

Les comptables n’utilisent pas de blanc

Disons que vous voulez donner 100 € à votre cousin Paul pour la forêt noire qu’il vous a fait parvenir depuis Marseille. Il y a longtemps, vous auriez dû voyager jusqu’à la cité fosséenne pour le payer en main propre ou vous auriez dû lui envoyer la somme par la poste. Ces deux méthodes s’avéraient considérablement pénibles et des alternatives se sont développées. Par exemple, les banques vous proposaient d’acheminer l’argent à Paul pour vous. Bien sûr, ce service engendrait un surcoût, mais vous étiez prêts à le payer pour éviter de voyager. En fait, la banque envoyait quelqu’un porter votre argent à la banque de Paul ; on faisait la même chose que vous auriez faite, mais par l’intermédiaire d’un préposé. Les banques pouvaient aussi regrouper les transferts en lots : plutôt que d’envoyer une personne à chaque fois, elle ne faisait plus qu’un voyage. En cas de problème, par exemple si le destinataire avait changé de banque (rappelez-vous qu’il fallait quatre jours aux postes du XVIIIe siècle pour faire Paris-Lyon-Marseille), l’argent était renvoyé au compte du débiteur.

Plus tard, le système bancaire moderne a été introduit et a évité de faire transiter physiquement l’argent sur les routes (au grand dam des bandits de grand chemin !). Les banques ont l’argent sur le papier, ce qui leur permet de le faire circuler dans envoyer d’objets de valeur. Cependant, le vieux concept est resté dans nos mémoires : pour envoyer de l’argent à quelqu’un, notre banque doit sortir les billets de notre compte et les apporter au compte du créditeur. Aujourd’hui, nous sommes habitués à ce que cela se produise instantanément. Cela ne prend que quelques clics pour commander des biens sur Amazon et qu’ils soient expédiés, alors pourquoi une transaction bancaire prendrait-elle plus de temps ?

Les banques sont toutes électroniques de nos jours (et l’ont été depuis longtemps). Quand nous ordonnons un virement, nous nous attendons à ce qu’il parte immédiatement et nous voulons que cela fonctionne de la même manière qu’antan : prends l’argent de mon compte, dépose-le sur celui de Paul ou, si quelque chose se passe mal, rends-le-moi. Bien que ce soit la démarche logique, ce n’est pas vraiment ce qui se passe derrière le comptoir, et cela ne l’a jamais été depuis que les ordinateurs ont été utilisés par les banques.

Quand vous vous rendez à votre banque et lui demandez d’envoyer de l’argent à Paul, le comptable va amorcer une transaction en prenant note que vous avez demandé d’effectuer un virement. La transaction contiendra la date, la somme et le compte créditeur. Souvenez-vous que les banques doivent toujours être équilibrées : la somme déduite de votre compte ne peut pas se volatiliser. Le comptable va placer votre argent sur un compte de transit que la banque tient pour vous. Le solde de votre compte est alors une agrégation de votre solde courant et des transactions listées dans le compte de transit. Ensuite, la banque vérifie que les coordonnées bancaires de Paul sont correctes et que l’argent peut y être déposé sans problème. Si c’est le cas, l’argent est déplacé dans une autre transaction initiée depuis le compte de transit jusqu’au compte de Paul. Tout est équilibré. Notez qu’il y a plusieurs transactions indépendantes et non une seule qui contiendrait un ensemble d’actions.

Maintenant, considérons un cas d’erreur : le compte de Paul n’existe plus. La banque s’en rend compte lorsqu’elle traite le lot d’opérations qui contient toutes les transactions depuis les comptes de transit. Une deuxième transaction est alors générée, laquelle renvoie l’argent sur votre compte. Notez que la transaction qui a débité initialement votre compte n’est pas défaite, mais que c’est une nouvelle (troisième) transaction qui est créée.

Un autre cas d’erreur : vous n’avez pas assez d’argent sur votre compte pour envoyer 100 € à Paul. Ce sera vérifié par le comptable (ou l’équivalent logiciel) avant que la banque crée la première transaction débitrice. Pour la comptabilité, la banque ne peut pas prétendre qu’il ne s’est rien passé : elle doit garder la trace de l’opération et de son rejet dans un journal. Annuler une opération consiste à effectuer la transaction inverse et jamais en effaçant la première transaction. « Les comptables n’utilisent pas de blanc » est une citation de Pat Helland, un architecte des systèmes transactionnels qui a travaillé pour Microsoft et Amazon.

Pour récapituler, une transaction peut aboutir ou faillir, et rien d’autre. La seule opération que CouchDB garantit comme pouvant aboutir ou faillir est l’écriture d’un document. Toutes les opérations qui sont comprises dans une transaction doivent être contenues dans le même document. Si la logique métier détecte la survenance d’une erreur (par ex., il n’y a pas les fonds requis), une nouvelle transaction inverse doit être créée.

Voyons ce qu’il en est avec CouchDB. Nous avons mentionné que le solde de votre compte est une valeur agrégée. Si nous nous en tenons à cette image, les choses sont simples. Plutôt que mettre à jour le solde des deux comptes (le vôtre et celui de Paul, ou le vôtre et celui de transit), nous créons une seule transaction qui décrit ce que nous sommes en train de faire et recourons à une vue pour obtenir le solde agrégé.

Considérons les transactions suivantes :

...
{"from":"Jan","to":"Paul","amount":100}
{"from":"Paul","to":"Steve","amount":20}
{"from":"Work","to":"Jan","amount":200}
...

Dans CouchDB, l’écriture d’un document est une opération atomique. Interroger une vue oblige à mettre à jour l’index de la vue en prenant en compte l’ensemble des changements survenus dans tous les documents. La vue résultante est toujours cohérente avec les données présentes dans nos documents. Cela garantit que notre banque est toujours équilibrée. Bien sûr, il existe bien d’autres transactions, mais celles-ci suffiront pour l’exemple.

Comment consultons-nous le solde courant ? C’est simple : une vue MapReduce.

function(transaction) {
  emit(transaction.from, transaction.amount * -1);
  emit(transaction.to, transaction.amount);
}
function(keys, values) {
  return sum(values);
}

Ça n’a pas l’air bien compliqué, n’est-ce pas ? Nous stockerons cela dans une vue balance (NdT : solde) dans un document _design/account. Consultons le solde de Jan :

curl 'http://127.0.0.1:5984/bank/_design/account/_view/balance?key="Jan"'

CouchDB répond :

{"rows":[
{"key":null,"value":100}
]}

Ça semble correct ! Maintenant, vérifions que notre banque est bien équilibrée. La somme des transactions devrait être égale à zéro :

curl http://127.0.0.1:5984/bank/_design/account/_view/balance

CouchDB répond :

{"rows":[
{"key":null,"value":0}
]}

Résumé

Cela devrait expliquer que les applications qui requièrent une forte cohérence peuvent utiliser CouchDB s’il est possible de découper les grosses transactions et petites. Une banque est une bonne approximation d’un métier sérieux, donc vous pouvez être rassurés quant à la possibilité de modéliser votre transaction importante en petites transactions CouchDB.

Trier les listes

Les vues vous permettent de trier les éléments quelle que soit la valeur de vos données — même les clés JSON complexes sont possibles, comme nous l'avons vu dans les chapitres précédents. Le tri par date s'avère très utile pour permettre aux utilisateurs de s'y retrouver rapidement ; un nom est plus facile à trouver dans une liste alphabétique. Les Hommes recourent naturellement à un algorithme de division et de conquête (cela vous semble-t-il familier ?) et ignorent une bonne partie des données, car ils savent que le nom qu'ils cherchent ne s'y trouvera pas. De la même manière, trier les nombres et les dates est d'une grande aide aux utilisateurs pour gérer leurs données en perpétuelle accumulation.

Il existe un autre type de tri un peu plus exotique. Les moteurs de recherche vous donnent les résultats par ordre de pertinence. Cette pertinence est ce que le moteur de recherche devine être le plus pertinente en fonction des mots-clés que vous recherchez (et aussi, potentiellement, votre historique de recherche ou de navigation). Il existe d'autres systèmes qui tentent d'inférer ce qui est le plus pertinent pour vous à partir d'anciennes données, mais ils ont la tâche quasi impossible de devenir ce qu'un utilisateur désire. Or, les ordinateurs sont d'exécrables devins.

La manière la plus simple pour un ordinateur de comprendre ce qui est le plus pertinent est de laisser l'utilisateur définir les priorités. Prenez une application de liste de tâches (de l'anglais, todo list) : elle permet aux utilisateurs de réordonner les tâches pour qu'ils sachent ce qu'ils ont à faire ensuite. La problématique sous-jacente, qui consiste à mémoriser l'ordre défini par un utilisateur, se retrouve dans de nombreux autres contextes.

Une liste d'entiers

Restons sur l'exemple de la liste de tâches. Une approche naïve consiste à assigner un nombre à chaque élément, lequel définit la position dans la liste. Nous utilisons une vue pour obtenir toutes les tâches dans le bon ordre.

D’abord, nous avons besoin de quelques documents :

{
  "title":"Remember the Milk",
  "date":"2009-07-22T09:53:37",
  "sort_order":2
}

{
  "title":"Call Fred",
  "date":"2009-07-21T19:41:34",
  "sort_order":3
}

{
  "title":"Gift for Amy",
  "date":"2009-07-19T17:33:29",
  "sort_order":4
}

{
  "title":"Laundry",
  "date":"2009-07-22T14:23:11",
  "sort_order":1
}

Ensuite, nous créons une vue avec une simple fonction qui émet les enregistrements, lesquels sont triés par le champ sort_order de nos documents. La vue qui en résulte correspond à ce que nous attendons :

function(todo) {
  if(todo.sort_order && todo.title) {
    emit(todo.sort_order, todo.title);
  }
}
{
  "total_rows": 4,
  "offset": 0,
  "rows": [
    {
      "key":1,
      "value":"Laundry",
      "id":"..."
    },
    {
      "key":2,
      "value":"Remember the Milk",
      "id":"..."
    },
    {
      "key":3,
      "value":"Call Fred",
      "id":"..."
    },
    {
      "key":4,
      "value":"Gift for Amy",
      "id":"..."
    }
  ]
}

Cela semble relativement simple, mais pouvez-vous repérer le problème ? Voici un indice : que devez-vous faire si recevoir un cadeau d’Amy devient plus prioritaire que de se souvenir du lait ? Conceptuellement, c’est simple :

  1. Affecter à “Gift for Amy” le sort_order de “Remember the Milk.”
  2. Incrémenter le sort_order de “Remember the Milk” et de tous les objets qui suivent, les uns après les autres.

Sous le capot, c’est une autre affaire. Avec CouchDB, vous devriez charger chaque document, en incrémenter le sort_order et le sauvegarder. Si vous avez de nombreuses choses à faire, comme moi, cela représente un travail conséquent. Peut-être y a-t-il une meilleure approche.

Une liste de réels

La solution est simple : au lieu d’utiliser un entier pour classer les objets, nous utilisons un flottant :

{
  "title":"Remember the Milk",
  "date":"2009-07-22T09:53:37",
  "sort_order":0.2
}

{
  "title":"Call Fred",
  "date":"2009-07-21T19:41:34",
  "sort_order":0.3
}

{
  "title":"Gift for Amy",
  "date":"2009-07-19T17:33:29",
  "sort_order":0.4
}

{
  "title":"Laundry",
  "date":"2009-07-22T14:23:11",
  "sort_order":0.1
}

La vue reste inchangée. La parcourir est aussi aisé. Réordonner les enregistrements est plus facile. L’application conserve une copie des sort_order, de sorte que lorsque l’on déplace un enregistrement, nous avons sa nouvelle position ainsi que celle des deux éléments qui l’entourent.

Déplaçons “Gift for Amy” pour le mettre au-dessus de “Remember the Milk”. Les valeurs des sort_order qui l’entourent sont 0.1 et 0.2. Pour stocker le bon sort_order de “Gift for Amy”, nous utilisons simplement la valeur médiane des deux éléments qui l’entourent : (0.1 + 0.2) / 2 = 0.3 / 2 = 0.15.

Si nous regardons à nouveau la vue, nous obtenons le résultat souhaité :

{
  "total_rows": 4,
  "offset": 0,
  "rows": [
    {
      "key":0.1,
      "value":"Laundry",
      "id":"..."
    },
    {
      "key":0.15,
      "value":"Gift for Amy",
      "id":"..."
    },
    {
      "key":0.2,
      "value":"Remember the Milk",
      "id":"..."
    },
    {
      "key":0.3,
      "value":"Call Fred",
      "id":"..."
    }
  ]
}

L’inconvénient de cette approche apparaît avec un grand nombre de remaniements : la précision des flottants peut devenir un problème, puisque les nombres « grandissent » indéfiniment. Une solution serait de ne pas s’en soucier, considérant qu’un utilisateur seul n’atteindra pas la limite. Autrement, une tâche d’administration peut redéfinir les index de la liste à des entiers quand personne n’utilise l’application.

L’avantage de cette approche est que vous n’avez qu’un seul document à mettre à jour, ce qui est efficace pour stocker le nouvel ordre d’une liste et mettre à jour la vue qui maintient l’index ordonné, puisqu’un seul document doit y être stocké.

Pagination

Cette recette explique comment paginer les résultats renvoyés par une vue. La pagination est un patron (de l’anglais, pattern) de l’IHM qui permet d’afficher un grand nombre d’enregistrements (le result set) sans devoir tout charger au début dans l’interface. Un sous-ensemble déterminé, la page, est affiché avec les boutons précédent et suivant, lesquels déplacent le viewport le long du result set jusqu’à couvrir la page souhaitée.

Nous considérons que vous savez créer et chercher des documents et des vues, et que vous êtes à l’aise avec les options des requêtes exécutées sur les vues.

Cas d’emploi

Afin d’avoir quelques données à manipuler, nous créons une liste de groupes de musique, à raison d’un document par groupe :

{ "name":"Biffy Clyro" }

{ "name":"Foo Fighters" }

{ "name":"Tool" }

{ "name":"Nirvana" }

{ "name":"Helmet" }

{ "name":"Tenacious D" }

{ "name":"Future of the Left" }

{ "name":"A Perfect Circle" }

{ "name":"Silverchair" }

{ "name":"Queens of the Stone Age" }

{ "name":"Kerub" }

Une vue

Nous avons besoin d’une fonction de subdivision (map) toute simple, qui nous donne la liste des groupes de musique, dans l’ordre alphabétique. Ce devrait être simple, mais nous y incorporons un peu de finesse en ignorant les « The » et « A » qui précèdent le nom :

function(doc) {
  if(doc.name) {
    var name = doc.name.replace(/^(A|The) /, "");
    emit(name, null);
  }
}

Cette vue donne la liste des groupes dans l’ordre alphabétique. À présent, nous voulons afficher cinq groupes à la fois et ajouter un lien vers les cinq suivants, lesquels forment une page, ainsi qu’un lien vers les cinq précédents si nous ne sommes pas à la première page.

Dans les chapitres précédents, nous avons appris à utiliser les paramètres startkey, limit et skip. Nous allons les réutiliser ici. Tout d’abord, regardons l’ensemble des résultats :

{"total_rows":11,"offset":0,"rows":[
  {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null},
  {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null},
  {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null},
  {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null},
  {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null},
  {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null},
  {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null},
  {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null},
  {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age","value":null},
  {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null},
  {"id":"fd590d4ad53771db47b0406054f02243","key":"Tool","value":null}
]}

Paramétrage

Le fonctionnement de la pagination est très simple :

Ou, en pseudo-JavaScript :

var result = new Result();
var page = result.getPage();

page.display();

if(result.hasPrev()) {
  page.display_link('prev');
}

if(result.hasNext()) {
  page.display_link('next');
}

Pagination lente (À ne surtout pas utiliser)

N’utilisez surtout pas cette méthode ! Nous la montrons simplement parce qu’elle semble naturelle, aussi devez-vous comprendre pourquoi c’est une mauvaise idée. Pour obtenir les cinq premiers enregistrements, utilisez le paramètre de requête ?limit=5 :

curl -X GET http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5

Résultat :

{"total_rows":11,"offset":0,"rows":[
  {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null},
  {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null},
  {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null},
  {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null},
  {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null}
]}

En comparant le nombre total de résultats (total_rows) à la limite actuelle (limit), nous pouvons déterminer si d’autres pages sont à afficher. Nous savons aussi, en lisant la valeur du décalage (offset), que nous sommes sur la première page. Nous pouvons calculer la valeur de skip= pour obtenir la page suivante :

var rows_per_page = 5;
var page = (offset / rows_per_page) + 1; // == 1
var skip = page * rows_per_page; // == 5 for the first page, 10 for the second ...

Donc, nous requêtons CouchDB avec :

curl -X GET 'http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5&skip=5'

Remarquez que nous devons utiliser ' (guillemet simple et droit) pour échapper l’esperluète &, laquelle est un caractère de commande dans le shell où nous lançons curl.

Résultat :

{"total_rows":11,"offset":5,"rows":[
  {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null},
  {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null},
  {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null},
  {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age","value":null},
  {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null}
]}

Implanter les méthodes hasPrev() et hasNext() est assez direct :

function hasPrev()
{
  return page > 1;
}

function hasNext()
{
  var last_page = Math.floor(total_rows / rows_per_page) +
    (total_rows % rows_per_page);
  return page != last_page;
}
Le problème

Tout cela paraît simple et direct, mais recèle un défaut fatal. Souvenez-vous de la manière dont les résultats d’une vue sont générés par l’index, donc l’arbre B sous-jacent : CouchDB saute le premier enregistrement (ou le premier enregistrement qui correspond à startkey si ce dernier est fourni) et lit chaque enregistrement de l’index l’un après l’autre jusqu’à la fin (ou jusqu’à atteindre limit ou endkey).

L’argument skip fonctionne ainsi : en plus d’aller au premier enregistrement et commencer à lire, skip va zapper autant d’enregistrement que demandé, mais CouchDB lira toujours depuis le premier enregistrement ; simplement, il ne renverra pas les valeurs des enregistrements zappés. Si vous spécifiez skip=100, CouchDB lira 100 enregistrements et ne créera aucune valeur correspondante. Cela ne semble pas catastrophique, mais ça l’est bel et bien, notamment quand vous utilisez 1000 ou 10000 comme valeur de skip. CouchDB devra lire toutes ces données futiles.

En règle générale, skip ne devrait être utilisé qu’avec une valeur à un chiffre. Bien qu’il soit possible qu’il existe des cas légitimes où il serait nécessaire d’utiliser une valeur supérieure, c’est habituellement révélateur d’un problème de conception. Enfin, pour que les calculs fonctionnent, vous devez ajouter une fonction d’agrégation (reduce) et effectuer deux appels à la vue pour obtenir la bonne numérotation, et il est encore possible que vous tombiez à côté.

Pagination rapide (À utiliser)

La bonne solution n’est pas beaucoup plus difficile. Plutôt que découper votre ensemble de résultats en pages de même taille, nous récupérons 10 enregistrements à la fois et utilisons startkey pour atteindre les 10 suivants. Nous utilisons même skip, mais seulement avec la valeur 1.

Voici comment cela fonctionne :

L’astuce qui permet de trouver la page suivante est assez simple. Plutôt que de demander 10 enregistrements pour chaque page, nous en demandons 11, mais n’en affichons que 10 ; la dernière valeur sert à donner la startkey à la page suivante. Même principe pour la page précédente : envoyer la startkey courante à la page suivante permet de savoir s’il y a une page précédente. S’il n’y a pas de startkey précédente, nous sommes sur la première page. De plus, nous n’affichons plus le lien vers la page suivante si nous récupérons un nombre d’enregistrements inférieur ou égal à rows_per_page. Cette méthode se nomme pagination par liste chaînée, puisque nous allons de page en page, ou de puce en puce (une puce étant un élément d’une liste), plutôt que d’aller sur une page précalculée. Il y a cependant un inconvénient. Le voyez-vous ?

Les clés utilisées par les vues de CouchDB n’ont pas à être uniques ; vous pouvez avoir plusieurs entrées d’index à la valeur read. Que se passe-t-il dans le cas où le nombre d’enregistrements correspondant à la clé read dépasse le nombre d’éléments affichés sur une page ? startkey saute sur le premier, si bien que vous l’auriez dans le baba si CouchDB n’offrait pas un deuxième paramètre pour vous sauver. Toutes les clés identiques d’une vue sont classées par docid, c’est-à-dire par l’identifiant du document qui a donné lieu à cet enregistrement dans la vue. Vous pouvez utiliser les paramètres startkey_docid et endkey_docid afin d’obtenir des sous-ensembles de ces enregistrements. Pour la pagination, nous n’avons pas besoin de endkey_docid, mais startkey_docid est très pratique. Vous l’ajoutez à startkey et limit pour gérer la pagination si et seulement si le onzième enregistrement présente la même valeur que votre startkey actuelle.

Il est essentiel de comprendre que les paramètres *_docid ne sont utiles qu’en complément des paramètres *key, car ils ne servent qu’à affiner la recherche sur une clé dans une vue. Ils ne fonctionnent pas de manière autonome (à l’exception de la vue all_docs qui est déjà triée par ID de document).

L’avantage de cette approche est de permettre des opérations ultrarapides sur l’index de l’arbre B qui sous-tend la vue. Obtenir une page ne nécessite pas de parcourir inutilement des centaines ou des milliers d’enregistrements.

Atteindre la page…

Un inconvénient de la pagination par liste chaînée est l’impossibilité de prédéterminer la clé qui permet d’accéder à la page n à partir du nombre d’enregistrements et de la taille d’une page. Cela n’est tout bonnement pas possible. Notre réponse à ce besoin, s’il survient, est de dire : « Même Google ne le fait pas ! » et de faire avec. Sur la première page de résultats, Google prétend toujours en avoir dix de plus. C’est seulement si vous cliquez sur la page numéro 2 (ce que peu de gens font) que Google pourrait vous indiquer qu’il y a en fait moins de pages. Si vous parcourez les pages, vous n’obtenez des liens que vers les 10 pages alentour ; jamais plus. Précalculer les startkey et startkey_docid est une opération possible ainsi qu’une optimisation pragmatique et préférable à calculer les clés d’un ensemble de résultats qui peut dépasser les dizaines de milliers d’enregistrements.

Si vous avez vraiment besoin de fournir un lien vers chaque page de résultat (nous avons vu des applications qui en avaient besoin), vous pouvez toujours maintenir un nombre entier en tant que clé utilisée par la vue et adopter une approche hybride concernant la pagination.