3

I want to implement pagination for mongodb in node.js enviroment using offical mongodb package. I tried to find out on internet but all are mongoose based links. I dont want to use mongoose.

How can I implement pagination using official client api given at
http://mongodb.github.io/node-mongodb-native/3.1/api/

Alok
  • 7,734
  • 8
  • 55
  • 100

4 Answers4

9

Offset-based approach has a big flaw: if the results list has changed between calls to the API, the indices would shift and cause an item to be either returned twice or skipped and never returned

This problem is demonstrated at https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/

Time-based pagination approach would be little better because results are no longer skipped. If you query the first page, and then a new item is deleted, it won’t shift the results in your second page and all is fine. However, this approach has a major flaw: what if there is more than one item that was created at the same time?

Best would be to use Cursor based pagination
Which can be implemented using any field in collection which is Unique, Orderable and Immutable.

_id satisfy all Unique, Orderable and Immutable conditions. Based on this field we can sort and return page result with _id of last document as the cusror for subsequent request.

curl https://api.mixmax.com/items?limit=2

const items = db.items.find({}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })

when the user wants to get the second page, they pass the cursor (as next) on the URL: curl https://api.mixmax.com/items?limit=2&next=590e9abd4abbf1165862d342

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })

If we want to return results in a different order, such as the date the item then we will add sort=launchDate to the querystring. curl https://api.mixmax.com/items?limit=2&sort=launchDate

const items = db.items.find({}).sort({
   launchDate: -1
}).limit(2);

const next = items[items.length - 1].launchDate;
res.json({ items, next })

For subsequent page request
curl https://api.mixmax.com/items?limit=2&sort=launchDate&next=2017-09-11T00%3A44%3A54.036Z

const items = db.items.find({
  launchDate: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1].launchDate;
res.json({ items, next });

If we launched a bunch of items on the same day and time? Now our launchDate field is no longer unique and doesn’t satisfy Unique, Orderable and Immutable. condition. We can’t use it as a cursor field. But we could use two fields to generate the cursor.Since we know that the _id field in MongoDB always satisfies the above three condition, we know that if we use it alongside our launchDate field, the combination of the two fields would satisfy the requirements and could be together used as a cursor field. curl https://api.mixmax.com/items?limit=2&sort=launchDate

const items = db.items.find({}).sort({
   launchDate: -1,
  _id: -1 // secondary sort in case there are duplicate launchDate values
}).limit(2);

const lastItem = items[items.length - 1];
// The cursor is a concatenation of the two cursor fields, since both are needed to satisfy the requirements of being a cursor field
const next = `${lastItem.launchDate}_${lastItem._id}`;
res.json({ items, next });

For subsequent page request
curl https://api.mixmax.com/items?limit=2&sort=launchDate&next=2017-09-11T00%3A44%3A54.036Z_590e9abd4abbf1165862d342

const [nextLaunchDate, nextId] = req.query.next.split(‘_’);
const items = db.items.find({
  $or: [{
    launchDate: { $lt: nextLaunchDate }
  }, {
    // If the launchDate is an exact match, we need a tiebreaker, so we use the _id field from the cursor.
    launchDate: nextLaunchDate,
  _id: { $lt: nextId }
  }]
}).sort({
   _id: -1
}).limit(2);

const lastItem = items[items.length - 1];
// The cursor is a concatenation of the two cursor fields, since both are needed to satisfy the requirements of being a cursor field
const next = `${lastItem.launchDate}_${lastItem._id}`;
res.json({ items, next });

Refefence: https://engineering.mixmax.com/blog/api-paging-built-the-right-way/

Alok
  • 7,734
  • 8
  • 55
  • 100
  • This is also called keyset pagination, read more about it here: https://use-the-index-luke.com/no-offset It is generally more performant and consistent, though it should be said, it has a few limitations that the offset pagination approach doesn't, an important one being, you can't navigate to an arbitrary page. – Miguel Santos Dec 03 '21 at 11:18
5

Using the recommended pagination approach with limit() and skip() (see here):

const MongoClient = require('mongodb').MongoClient;
MongoClient.connect('http:localhost:27017').then((client) => {
    const db = client.db(mongo.db);
    db.collection('my-collection').find({}, {limit:10, skip:0}).then((documents) => {
        //First 10 documents
        console.log(documents);
    });


    db.collection('my-collection').find({}, {limit:10, skip:10}).then((documents) => {
        //Documents 11 to 20
        console.log(documents);
    });
});

Here's a pagination function:

function studentsPerPage (pageNumber, nPerPage) {
    return db.collection('students').find({}, 
        {
            limit: nPerPage, 
            skip: pageNumber > 0 ? ( ( pageNumber - 1 ) * nPerPage ) : 0
        });
}
Miguel Santos
  • 344
  • 3
  • 9
  • 3
    It's kind of crazy that that's the recommended approach in mongo's docs! In almost any database, skip can be a bit dangerous with a large offset because it'll need to scan through all of the docs up to the ones that you're looking for (the docs, to their credit, do mention this). I think a better pattern might be ordering by the _id field, and passing the _id that you want to get the documents before -- `{_id: {$lte: previousId}}`. – klhr Oct 03 '19 at 23:46
  • The `skip` approach is simple & clear though, and if most people aren't going to be paging through lots of documents, it's probably not worth worrying about. – klhr Oct 03 '19 at 23:50
0

I am sending an API that is on MongoDb and Nodejs.

module.exports.fetchLoans = function(req, res, next) {
    var perPage = 5;
    var page = req.body.page || 1;
    loans
      .find({ userId: req.user._id})
      .select("-emi")
      .skip(perPage * page - perPage)
      .limit(perPage)
      .sort({ timestamp: -1 })
      .exec(function(err, loan) {
        if (loan != null) {
          loans
            .find({ userId: req.user._id})
            .count()
            .exec(function(err, count) {
              if (count != null) {
                res.json({
                  success: true,
                  loans: loan,
                  currentpage: page,
                  totalpages: Math.ceil(count / perPage)
                });
              } else {
                console.log("Milestone Error: ", err);
                res.json({ success: false, error: "Internal Server Error. Please try again." });
              }
            });
        } else {
          console.log("Milestone Error: ", err);
          res.json({ success: false, error: "Internal Server Error. Please try again." });
        }
      });
  };

In this code, you will have to provide page number on every hit.

Ankit Kumar Rajpoot
  • 5,188
  • 2
  • 38
  • 32
0

You can use skip and limit options to implement pagination

module.exports = (data)=>{

  let page = parseInt(data.page);
  let limit = parseInt(data.limit);
  let skip = 0

  if(page>1){
   skip = (page * limit);
   }


let mongoClient = require('mongodb').MongoClient;
    mongoClient.connect('mongodb://localhost:27017').then((client) => {
        let db = client.db('your-db');
        db.collection('your-collection').find({}, {limit:limit, skip:skip}).then((documents) => {
            console.log(documents);
        });

    });
};
ArUn
  • 1,317
  • 2
  • 23
  • 39