javascriptpouchdb

Pouchdb pagination


I am looking for a way to paginate in pouchdb by specifying the number of the page that I want.

The closest example I came across is this:

var options = {limit : 5};
function fetchNextPage() {
  pouch.allDocs(options, function (err, response) {
    if (response && response.rows.length > 0) {
      options.startkey = response.rows[response.rows.length - 1].id;
      options.skip = 1;
    }
  });
}

It assumes however that you are paginating one page after the other and calling this consecutively several times.

What I need instead is a way to retrieve page 5 for example, with a single query.


Solution

  • There is no easy answer to the question. A small slice from 3.2.5.6. Jump to Page

    One drawback of the linked list style pagination is that you can’t pre-compute the rows for a particular page from the page number and the rows per page. Jumping to a specific page doesn’t really work. Our gut reaction, if that concern is raised, is, “Not even Google is doing that!” and we tend to get away with it. Google always pretends on the first page to find 10 more pages of results. Only if you click on the second page (something very few people actually do) might Google display a reduced set of pages. If you page through the results, you get links for the previous and next 10 pages, but no more. Pre-computing the necessary startkey and startkey_docid for 20 pages is a feasible operation and a pragmatic optimization to know the rows for every page in a result set that is potentially tens of thousands of rows long, or more.

    If you are lucky and every document has an ordered sequence number, then a view could be constructed to easily navigate pages.

    Another strategy is to precompute (preload) a range of keys, which is more reasonable but is complicated. The snippet below creates a trivial database which can be paged through via nav links.

    There are 5 documents per page, and each "chapter" has 10 pages. computePages performs the look ahead

    // look ahead and cache startkey for pages.
    async function computePages(startPage, perPage, lookAheadPages, startKey) {
      let options = {
        limit: perPage * lookAheadPages,
        include_docs: false,
        reduce: false
      };
      // adjust. This happens when a requested page has no key cached.
      if (startKey !== undefined) {
        options.startkey = startKey;
        options.skip = perPage; // not ideal, but tolerable probably?
      }
      const result = await db.allDocs(options);
      // use max to prevent result overrun
      // only the first key of each page is stored
      const max = Math.min(options.limit, result.rows.length)
      for (let i = 0; i < max; i += perPage) {
        page_keys[startPage++] = result.rows[i].id;
      }
    }
    

    page_keys provides a key/value store mapping page number to start key. Usually anything other than 1 for skip is red flag however this is reasonable here - we won't be skipping say a 100 documents right?

    I just threw this together so it is imperfect and likely buggy, but it does demonstrate page navigation generally.

    function gel(id) {
      return document.getElementById(id);
    }
    
    // canned test documents
    function getDocsToInstall() {
      let docs = [];
      // doc ids are a silly sequence of characters.
      for (let i = 33; i < 255; i++) {
        docs.push({
          _id: `doc-${String.fromCharCode(i)}`
        });
      }
      return docs;
    }
    
    // init db instance
    let db;
    async function initDb() {
      db = new PouchDB('test', {
        adapter: 'memory'
      });
      await db.bulkDocs(getDocsToInstall());
    }
    
    // documents to show per page
    const rows_per_page = 5;
    // how many pages to preload into the page_keys list.
    const look_ahead_pages = 10;
    // page key cache: key = page number, value = document key
    const page_keys = {};
    // the current page being viewed
    let page_keys_index = 0;
    // track total rows available to prevent rendering links beyond available pages.
    let total_rows = undefined;
    async function showPage(page) {
      // load the docs for this page
      let options = {
        limit: rows_per_page,
        include_docs: true,
        startkey: page_keys[page] // page index is computed
      };
      let result = await db.allDocs(options);
      // see renderNav. Here, there is NO accounting for live changes to the db.
      total_rows = total_rows || result.total_rows;
      // just display the doc ids.
      const view = gel('view');
      view.innerText = result.rows.map(row => row.id).join("\n");
    }
    
    // look ahead and cache startkey for pages.
    async function computePages(startPage, perPage, lookAheadPages, startKey) {
      let options = {
        limit: perPage * lookAheadPages,
        include_docs: false,
        reduce: false
      };
      // adjust. This happens when a requested page has no key cached.
      if (startKey !== undefined) {
        options.startkey = startKey;
        options.skip = perPage; // not ideal, but tolerable probably?
      }
      const result = await db.allDocs(options);
      // use max to prevent result overrun
      // only the first key of each page is stored
      const max = Math.min(options.limit, result.rows.length)
      for (let i = 0; i < max; i += perPage) {
        page_keys[startPage++] = result.rows[i].id;
      }
    }
    
    
    // show page links and optional skip backward/forward links.
    let last_chapter;
    async function renderNav() {
      // calculate which page to start linking.
      const chapter = Math.floor(page_keys_index / look_ahead_pages);
      if (chapter !== last_chapter) {
        last_chapter = chapter;
        const start = chapter * look_ahead_pages;
        let html = "";
        // don't render more page links than possible.
        let max = Math.min(start + look_ahead_pages, total_rows / rows_per_page);
    
        // render prev link if nav'ed past 1st chapter.
        if (start > 0) {
          html = `<a href="javascript:void" onclick="navTo(${start-1})">&lt;</a>&nbsp;&nbsp;`;
        }
    
        for (let i = start; i < max; i++) {
          html += `<a href="javascript:void" onclick="navTo(${i})">${i+1}</a>&nbsp;`;
        }
        // if more pages available, render the 'next' link
        if (max % look_ahead_pages === 0) {
          html += `&nbsp;<a href="javascript:void" onclick="navTo(${start+look_ahead_pages})">&gt;</a>&nbsp;`;
        }
    
        gel("nav").innerHTML = html;
      }
    }
    
    async function navTo(page) {
      if (page_keys[page] === undefined) {
        // page key not cached - compute more page keys.
        await computePages(page, rows_per_page, look_ahead_pages, page_keys[page - 1]);
      }
      page_keys_index = page;
      await showPage(page_keys_index);
      renderNav();
    
    }
    
    initDb().then(async() => {
      await navTo(0);
    });
    <script src="https://cdn.jsdelivr.net/npm/pouchdb@7.1.1/dist/pouchdb.min.js"></script>
    <script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.memory.min.js"></script>
    <pre id="view"></pre>
    <hr/>
    <div id="nav">
      </nav>