0

I am developing an application for Android that involves uploading and downloading images to a server. I am using Firebase for this purpose. All the images are stored in Firebase storage using the image hash as a filename. I also maintain a Realtime Database that stores some information about the pictures. I need to select a random picture from the storage, so I decided to select a random entry from the database. I found this answer on SO (as you see, int is written in Javascript), which suggests the following code:

const numberOfUsers = 15;
const randomIndex = Math.floor(Math.random() * numberOfUsers);
var ref = firebase.database().ref('companies/01/users');
ref.limitToFirst(randomIndex).limitToLast(1).once('value').then(snapshot =>
{
    var user = snapshot.val();
    // do something with the user data
});

but when I try to build a similar query in Java

imgref.limitToFirst(random + 1).limitToLast(1).addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            randomHash = dataSnapshot.getKey();
            Log.e("get random name: ", randomHash);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

        }
    });

I recieve an error

Can't call limitToLast on query with previously set limit!

So obviously I cannot use limitToFirst and LimitToLast in one query. So here is my question:

  1. Is it possible to use limitToFirst and limitToLast together somehow to use this method of getting a random entry?

  2. Is there any other method to get a random entry from a Firebase Realtime Database without downloading the whole database or iterating through it (as far as I understand, iterating is inefficient, if I am wrong, please explain)

  3. If it is impossible to do this from inside the app, is there some other way (for example, using Firebase Cloud Functions)

Petr
  • 259
  • 4
  • 18
  • Possible duplicate of [how can i retrieve a random value Firebase database](https://stackoverflow.com/questions/48245706/how-can-i-retrieve-a-random-value-firebase-database) – Alex Mamo May 17 '18 at 20:19
  • 1
    Please check the duplicate too se how it can be done. – Alex Mamo May 17 '18 at 20:19
  • @AlexMamo, that solution iterates through the whole table, as far as I understand, which is inefficient. Please correct me if I am wrong, as I am not an expert in how iterating a database works – Petr May 17 '18 at 20:24
  • @AlexMamo does iterating through entries download the data from the entries? – Petr May 18 '18 at 03:44
  • When you add a listener on the reference, you are donwloading the entire object that the listener points to. In the link above, you are getting the children beneath that node, iterate using `getChildren()` method and pick a random result from it. – Alex Mamo May 18 '18 at 07:47
  • I don't think that this answer will help you solve this issue because there is no connection between the `newRandomLong` from the reference and the one from your database. I just answered a similar question [here](https://stackoverflow.com/questions/50413117/how-to-get-unique-random-product-in-node-firebase/50413208) which I think it's even better than the duplicate. – Alex Mamo May 18 '18 at 14:55
  • @AlexMamo that answer involves iterating through the database. I chose the solution with auto-increment with a transaction, which works fine. I still appreciate your effort though – Petr May 18 '18 at 17:23

2 Answers2

1

If you aren't against adding an extra child to your data, you have a few options.

  1. You could use a transaction to number each item when they are added, then use a random number with startAt(random).limitToFirst(1).
  2. If you want to avoid the transaction you could write a random long to each imgref node. Then you could try querying with a newly generated random like this:

    ref.orderByChild("randomLong").startAt(newRandomLong).limitToFirst(1).addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            if(dataSnapshot.getChildrenCount() > 0){
                for(DataSnapshot snap : dataSnapshot.getChildren()){
                    //Do something with your random snapshot
                }
            }else
            {
             /*
             Handle case where the new random Long is after the range 
             currently in the database
             ref.orderByChild("randomLong").endAt(random).limitToLast(1)
             */
            }
        }
    
        @Override
        public void onCancelled(DatabaseError databaseError) {
    
        }
    });
    

Note that #2 will add random weights to your values based on the random ranges between values. This effect should be less noticeable on larger lists, but if you need an even distribution you probably want to go with option #1.

If you can't change your current data structure then I'm not sure you can get around iterating the set as in Alex's answer.

Cody W.
  • 376
  • 2
  • 6
  • I'm OK with adding a new child, but I think your solution has a couple of problems. Firstly, if two entries get the same random ID's, one of them will never be chosen. Secondly, if the entries have, for example, the following ID's: (100, 110), then the entry with the 100 value has a much higher probability of being chosen, because any random number from 0 to 100 will choose the first number, while only numbers from 101 to 110 will choose the second – Petr May 18 '18 at 03:43
  • I just was edited my solution to mention the 2nd problem you mentioned. The first is unlikely to be a problem using Longs since collisions are very unlikely until your list is huge. – Cody W. May 18 '18 at 03:45
0

You need to know how is your model to get a random key. If it is 1,2,3,4,5 ids you can use it like you are doing, so you can compare it. The best way to do it, is to store all key values in a array, pick a random number with the array size and pick the index of the random number. Them u use the code from the similar answer dataSnapshot.getKey();

Gaspar
  • 1,515
  • 13
  • 20