web-dev-qa-db-fra.com

Requête de plage pour la pagination MongoDB

Je souhaite implémenter la pagination sur une MongoDB. Pour ma requête de plage, j'ai pensé à utiliser des ObjectID:

db.tweets.find({ _id: { $lt: maxID } }, { limit: 50 })

Cependant, selon la documentation , la structure de l'ObjectID signifie que "les valeurs ObjectId ne représentent pas un ordre d'insertion strict":

La relation entre l'ordre des valeurs ObjectId et le temps de génération n'est pas stricte en une seconde. Si plusieurs systèmes, ou plusieurs processus ou unités sur un même système, génèrent des valeurs en une seconde; Les valeurs ObjectId ne représentent pas un ordre d'insertion strict. Le décalage d'horloge entre les clients peut également entraîner un ordre non strict, même pour les valeurs, car les pilotes de client génèrent des valeurs ObjectId, pas le processus mongod.

J'ai alors pensé à interroger avec un horodatage:

db.tweets.find({ created: { $lt: maxDate } }, { limit: 50 })

Cependant, rien ne garantit que la date sera unique - il est fort probable que deux documents puissent être créés dans la même seconde. Cela signifie que des documents peuvent être manqués lors de la pagination.

Existe-t-il une sorte de requête à distance qui me procurerait plus de stabilité?

35
user1082754

Il est parfaitement correct d'utiliser ObjectId () bien que votre syntaxe de pagination soit fausse. Tu veux:

 db.tweets.find().limit(50).sort({"_id":-1});

Cela signifie que vous souhaitez que les tweets soient triés par ordre _id dans l'ordre décroissant et que vous souhaitiez les 50 derniers. Votre problème est le fait que la pagination est délicate lorsque le jeu de résultats actuel change. Par conséquent, plutôt que de passer à la page suivante, vous voulez notez le plus petit _id dans le jeu de résultats (la 50ème valeur _id la plus récente, puis obtenez la page suivante avec:

 db.tweets.find( {_id : { "$lt" : <50th _id> } } ).limit(50).sort({"_id":-1});

Cela vous donnera les prochains "plus récents" tweets, sans que les nouveaux tweets entrants ne perturbent votre pagination dans le temps.

Il n'y a absolument aucune raison de s'inquiéter de savoir si la valeur de _id correspond strictement à l'ordre d'insertion - elle sera suffisamment proche de 99,999% et personne ne se soucie réellement du niveau inférieur à la seconde où Tweet est arrivé en premier - vous remarquerez peut-être même que Twitter affiche fréquemment des tweets hors d'usage, ce n'est tout simplement pas si critique.

Si est critique, vous devrez alors utiliser la même technique mais avec "date de Tweet" où cette date devra être un horodatage, plutôt qu'une simple date.

55
Asya Kamsky

L'horodatage "réel" de votre Tweet (c'est-à-dire l'heure tweetée et les critères de tri souhaités) ne serait-il pas différent de l'horodatage "insertion" de Tweet (c'est-à-dire l'heure ajoutée à la collection locale). Cela dépend de votre application, bien sûr, mais il est probable que les insertions de Tweet soient groupées ou sinon insérées dans le "mauvais" ordre. Ainsi, à moins de travailler sur Twitter (et d'avoir accès aux collections insérées dans le bon ordre), vous ne pourrez pas vous appuyer uniquement sur $natural ou ObjectID pour le tri logique. 

Les documents Mongo suggèrent skip et limit pour la pagination :

db.tweets.find({created: {$lt: maxID}).
          sort({created: -1, username: 1}).
          skip(50).limit(50); //second page

Il existe toutefois un problème de performances lors de l'utilisation de skip:

La méthode cursor.skip() est souvent coûteuse, car le serveur doit marcher au début de la collection ou de l'index pour obtenir la position offset ou ignorer avant de commencer à renvoyer le résultat. À mesure que le décalage augmente, cursor.skip() devient plus lent et consomme davantage de temps-processeur.

Cela est dû au fait que skip ne s'intègre pas dans le modèle MapReduce et n'est pas une opération qui évolue correctement. Vous devez donc attendre qu'une collection triée soit disponible avant de pouvoir être "découpée en tranches". Now limit(n) sonne comme une méthode tout aussi mauvaise car elle applique une contrainte similaire "à partir de l'autre extrémité"; Cependant, avec le tri appliqué, le moteur est en mesure d'optimiser quelque peu le processus en ne gardant en mémoire que les éléments n par fragment lors de son parcours dans la collection.

Une alternative consiste à utiliser la pagination basée sur une plage. Après avoir récupéré la première page de tweets, vous savez quelle est la valeur created pour le dernier tweet. Il vous suffit donc de remplacer l'original maxID par cette nouvelle valeur:

db.tweets.find({created: {$lt: lastTweetOnCurrentPageCreated}).
          sort({created: -1, username: 1}).
          limit(50); //next page

Effectuer une condition find comme celle-ci peut être facilement parallélisé. Mais comment traiter les pages autres que la suivante? Vous ne connaissez pas la date de début des pages 5, 10, 20 ou même la page précédente! @SergioTulentsev suggère un enchaînement créatif de méthodes mais je préconiserais le pré-calcul des plages avant-dernier du champ d'agrégat dans une collection pages séparée; ceux-ci pourraient être recalculés lors de la mise à jour. De plus, si vous n'êtes pas satisfait de DateTime (notez les remarques sur les performances) ou si vous craignez les valeurs en double, vous devez envisager les index composés sur l'attache timestamp + account (puisqu'un utilisateur ne peut pas tweeter deux fois. en même temps), ou même un agrégat artificiel des deux:

db.pages.
find({pagenum: 3})
> {pagenum:3; begin:"01-01-2014@BillGates"; end:"03-01-2014@big_ben_clock"}

db.tweets.
find({_sortdate: {$lt: "03-01-2014@big_ben_clock", $gt: "01-01-2014@BillGates"}).
sort({_sortdate: -1}).
limit(50) //third page

Utiliser un champ agrégé pour trier travaillera "sur le pli" (même s’il existe peut-être plus de façons casher de traiter la situation). Cela pourrait être configuré comme un index unique avec des valeurs corrigées au moment de l'insertion, avec un seul document Tweet ressemblant à

{
  _id: ...,
  created: ...,    //to be used in markup
  user: ...,    //also to be used in markup
  _sortdate: "01-01-2014@BillGates" //sorting only, use date AND time
}
10
o.v.

ObjectIds devrait être suffisant pour la pagination si vous limitez vos requêtes à la seconde précédente (ou ne vous souciez pas de la possibilité d'une seconde d'étrangeté). Si cela ne convient pas à vos besoins, vous devrez alors mettre en place un système de génération d’ID fonctionnant comme une incrémentation automatique. 

Mettre à jour:

Pour interroger la seconde précédente d'ObjectIds, vous devrez créer un ObjectID manuellement.

Voir la spécification de ObjectId http://docs.mongodb.org/manual/reference/object-id/

Essayez d'utiliser cette expression pour le faire à partir d'un mongos.

{ _id : 
  {
      $lt : ObjectId(Math.floor((new Date).getTime()/1000 - 1).toString(16)+"ffffffffffffffff")
  }

}

Les "f" à la fin doivent utiliser le maximum de bits aléatoires possibles qui ne sont pas associés à un horodatage puisque vous effectuez une requête inférieure à la requête.

Je le recommande lors de la création de l'ObjectId sur votre serveur d'applications plutôt que sur le mongos, car ce type de calcul peut vous ralentir si vous avez plusieurs utilisateurs.

0
Daniel Williams

J'ai construit une pagination en utilisant mongodb _id de cette façon.

// import ObjectId from mongodb
let sortOrder = -1;
let query = []
if (prev) {
    sortOrder = 1
    query.Push({title: 'findTitle', _id:{$gt: ObjectId('_idValue')}})
}

if (next) {
    sortOrder = -1
    query.Push({title: 'findTitle', _id:{$lt: ObjectId('_idValue')}})
}

db.collection.find(query).limit(10).sort({_id: sortOrder})
0