16

Let's say I have some data as follows:

{
    "name": "post #1",
    "votes": 100
},
{
    "name": "post #2",
    "votes": 10000
},
{
    "name": "post #3",
    "votes": 750
}

And so on.

The data in firebase is not ordered by votes obviously, it's order by key.

I'd like to be able to paginate this data by the number of votes in descending order.

I think I'd have to do something like:

ref.orderByChild('votes')
    .startAt(someValue)
    .limitToFirst(someOtherValue)
    .once('value', function(snapshot) {
        resolve(snapshot);
    });

Or perhaps, I'd need to use limitToLast, and endAt?

Pagination by key is quite easy, but this one is stumping me.

Update (2017-10-16): Firebase recently announced Firestore - their fully managed NoSQL datastore which should make it easier to do more complex queries. There may still be some use cases where one might use the Realtime database they've always provided. If you're one of those people this question should still apply.

Tyler Biscoe
  • 2,322
  • 3
  • 20
  • 33
  • See http://stackoverflow.com/questions/27195851/how-to-retrieve-paginated-children-in-ascending-or-descending-order-in-firebase/33484784#33484784 – Frank van Puffelen Jun 24 '16 at 04:31
  • @FrankvanPuffelen Your link is to a question which then links back to my answer below. Might you guys consider adding this information to the Firebase docs? – Tyler Biscoe Jan 23 '17 at 20:58

1 Answers1

38

I posted this question after a long day of coding and my brain was mush.

I took another crack at it this morning, and as is so often the case, was able to find the solution rather quickly.

Solution:

In order to paginate data, we need the ability to arbitrarily pull sorted data from the middle of a list. You can do this using the following functions provided by the firebase API:

startAt

endAt

limitToFirst

limitToLast

More information can be found here.

In my case, since I need my data in descending order, I'll use endAt and limitToLast.

Assuming I'd like each of my pages to have five items. I would write my first query like so:

ref.orderByChild('votes')
    .limitToLast(5)
    .once('value', function(snapshot) {
      console.log('Snap: ', snapshot.val());
    });

This gets the 5 items with the highest number of votes.

In order to get the next page, we'll need to store two pieces of data from the results of our last query. We need to store the number of votes, and key for the item that had the least number of votes. That is to say, the last item in our list.

Using that data, our next query, and all subsequent queries, would look as follows:

ref.orderByChild('votes')
    .endAt(previousVoteCount, previousKey)
    .limitToLast(5)
    .once('value', function(snapshot) {
      console.log('Snap: ', snapshot.val());
    });

You'll notice in this query I added endAt. This gets the next 5 items with the most votes starting from the last one in the previous list. We must include the the key for the last item in the previous list so that if there are multiple items with the same number of votes, it'll return a list beginning with the correct item, and not the first one it finds with that number of votes, which could be somewhere in the middle of the list you got from the initial query.

That's it!

Tyler Biscoe
  • 2,322
  • 3
  • 20
  • 33
  • Is this solution works with many same-value items? For example following scores: [5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7]. In a real world scenario, not many high-voted post will share the same value, but a LOT of low-score values will. This issue is commented in my SO question highlighted by @frank-van-puffelen above. – Pandaiolo Jun 26 '16 at 13:13
  • Assuming you're storing your data in a single list with firebase keys then the orderByChild part of the query will sort by 'votes' first, then should there be many posts with the same number of votes, it'll sort by key. – Tyler Biscoe Jun 26 '16 at 17:13
  • 1
    So that means, if you include the 'previousKey' argument in the 'endAt' part of the query, you'll be able to paginate accurately. – Tyler Biscoe Jun 26 '16 at 17:14
  • 3
    AH - yes... with the key indeed :-) I always thought `endAt()` only took one parameter, the value. Makes sense, i'll update my other post! – Pandaiolo Jun 27 '16 at 13:29
  • @Tyler thank you very much for this solution +1. i only had one question , what if i wanted to do your solution but i need to specify another key value , for example we added type to your data scheme , { "name","vote" , "type"} and i want type=3 , is there a way to apply your pagenation solution and add type =3 . ? – user4o01 Jul 14 '16 at 14:53
  • I'm not aware of a way to do that using the Firebase SDK. In my experience using firebase, the solution to your problem would be to change the way you're storing your data. So instead of having one long flat list that contains all item types, you'd split the list up by item type and paginate off of each branch? Does that make sense? Take a look at this gist if it isn't clear: https://gist.github.com/biscoe916/59b9dbd00a0a92e19698a9df9beb85f7 – Tyler Biscoe Jul 14 '16 at 18:34
  • In some cases, when coming to firebase or other nosql datastores, you sort of need to rethink the way you store data. – Tyler Biscoe Jul 14 '16 at 18:37
  • But how to add this newly retrived data to the bottom of the recylerview – Shifatul Sep 26 '16 at 15:57
  • 1
    @TylerBiscoe Thanks for the thorough explanation. The only question I have is if you pull the last 5 results, won't they still come back in ascending order? (because orderByChild returns in ascending order). I have read that it is still required to do reverse ordering on the client, unless you do a negative timestamp on Firebase. Thanks for reading! – Renee Olson May 16 '17 at 23:39