0

I use Cloud Firestore and this is my data structure :

   Firestore-root
   |
   --- collection (collection)
         |
         --- uid (document)
              |
              --- name: some_name
              |
              --- some_fields: some_data
              |
              --- origin: [origin_1, origine_2, ...] <- List
              |
              --- category: [category_1, category_2, ...] <- List
              |
              --- meals: [meals_1, meals_2, ...] <- List
              |
              --- some_other_filters: [.....] <- List
              |
              --- all_categories: [origin_1:true, origine_2:true, meals_1:true, ....]

I created the Map field all_categories to hold all different filters (origin, category, ...) so that I can perform queries easily like: if user select meals_4 and category_2, category_6 and some_other_filter I can do:

List<String> filters = getFilters(); 

query = recipeRepository.getUsersRecipeCollection()
            .orderBy(sorted_filter, DESCENDING).limit(5);

for (String item: filters)
    query = query.whereEqualTo("all_categories."+item, true);

query.get().addOnCompleteListener(...)

I cannot use the whereArrayContainsAny function since I need to get the exact items based on selected filters. What I need is something like "whereArrayContainsExactly.."

So, for example, if a user select 1, 2,.. or 25 different items from my filter page (like: category_1, meals_2, origin_1,... ), does it mean I need to create all 25 combinations of the composite indexes (through the link I get from the console) ? or I create 25 indexes of that map fields?

Second attempt:

I denormalised the data as following:

The collection that hold all the data:

enter image description here

The filter collections:

enter image description here

If I select meals_1 and origin_1, With whenAllSuccess I can get all document ids like the following:

Iterator<String> iterator = filters.iterator();
        List<Task> tasks = new ArrayList<>();
        while (iterator.hasNext()) {
            String filter = iterator.next();
            tasks.add(recipeRepository.getFilterCollection("filter_"+filter).get());
        }

        Tasks.whenAllSuccess(tasks.toArray(new Task[tasks.size()]))
                .addOnSuccessListener(new OnSuccessListener<List<Object>>() {
                    @Override
                    public void onSuccess(List<Object> objects) {
                        if (!objects.isEmpty()) {

                            for (Object querySnapshot: objects) {
                                QuerySnapshot querySnap = (QuerySnapshot)querySnapshot;

                                for (DocumentSnapshot id: querySnap.getDocuments()) {
                                    doc_ids.add(id.getId());
                                }
                            }

                            CollectionReference collectionReference = recipeRepository.getUsersRecipeCollection();

                            List<DocumentReference> listDocRef = new ArrayList<>();
                            for (String id: doc_ids) {
                                DocumentReference docRef = collectionReference.document(id);
                                listDocRef.add(docRef);
                            }

                            List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
                            for (DocumentReference documentReference : listDocRef) {
                                Task<DocumentSnapshot> documentSnapshotTask = documentReference.get();
                                tasks.add(documentSnapshotTask);
                            }

                            Tasks.whenAllSuccess(tasks).addOnSuccessListener(new OnSuccessListener<List<Object>>() {
                                @Override
                                public void onSuccess(List<Object> list) {
                                    Log.e(TAG, ">> list objects: " + list.size() + " data: " + list);
                                    for (Object object : list) {
                                        Recipes recipes = ((DocumentSnapshot) object).toObject(Recipes.class);

                                        if (recipes == null) continue;

                                        Log.e(TAG, ">> recipe name: " + recipes.getName());
                                    }
                                }
                            });
                        }
                    }
                });

With this I get all data containing meals_1 OR origin_1 (same result as using whereArrayContainsAny).

What I need is to get the list of docs ids that exists in both meals_1 and origin_1 collections.

Walid
  • 700
  • 2
  • 10
  • 29
  • What do you mean through "if a user select every time 1 - 25 different items from my filter page, does it mean I need to create all 25 combinations of the items ? "? – Alex Mamo Jul 21 '20 at 13:13
  • Hi @Alex, I edited that line, I hope it's more comprehensible. – Walid Jul 21 '20 at 14:15

1 Answers1

3

So, for example, if a user select 1, 2,.. or 25 different items from my filter page (like: category_1, meals_2, origin_1,... ), does it mean I need to create all 25 combinations of the composite indexes (through the link I get from the console) ? or I create 25 indexes of that map fields?

As I see in the for loop, at each iteration you are creating a new Query object, and this is because Cloud Firestore queries are immutable. Because you are using in the same query a .orderBy() call along with a .whereEqualTo() call, an index is required.

And to answer your question, yes, an index is required for each and very different query that you perform.

Edit:

An alternative solution might be to denormalize the data, by adding separate collections of each type of meal, and every time a filter is added you should create a new query for the selected collection. This is a common practice when it comes to NoSQL databases.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Thank you for your answer, can you please suggest me an alternative solution ? the thing is I need to do an AND operator on the filter elements, not an OR operator, thats why I cannot use whereArrayContainsAny – Walid Jul 22 '20 at 08:37
  • 1
    Thank you @Alex, I will try what you suggest And I'll come back. :) (+1 for your Edit) – Walid Jul 22 '20 at 08:50
  • Knowing that you'll have to deal with multiple queries, I also recommend you see my answer from this **[post](https://stackoverflow.com/questions/50118345/firestore-merging-two-queries-locally/50132229)**. `whenAllSuccess` will help get the result of multiple queries at once. – Alex Mamo Jul 22 '20 at 09:46
  • Thank you for this suggestion. By doing so ( denormalization and using "whenAllSuccess"), if I select 8 different filters, I will create 8 different queries, and i don't have to create indexes for every combinations, is that right ? – Walid Jul 22 '20 at 09:59
  • If you don't perform queries that require an index, yes, there is no need for that. If you perform simple queries, then no indexes are needed. – Alex Mamo Jul 22 '20 at 10:09
  • If I understood correctly (because I'm really stuck), I should create for each single filter item a separate collection, and each collection holds the Document id. but then I can not user orderBy, is my understanding right ? – Walid Jul 22 '20 at 10:52
  • 1
    Oh, yes you can. A simple [orderBy](https://firebase.google.com/docs/firestore/query-data/order-limit-data) doesn't require an index to be created. It is created automatically for you. – Alex Mamo Jul 22 '20 at 10:58
  • Hi Walid! Is there everything alright, have you solved the problem? – Alex Mamo Jul 23 '20 at 07:00
  • Hi Alex, unfortunately no.. I created for each single filter a collection (about 40), and in each collection the coresponded documentId. when I combine the queries, I need to create an index for each filter, since I do orderby (for sorting by date/relevance..) + whereEqualTo.. Am I missing something ? – Walid Jul 23 '20 at 08:59
  • 1
    Why would you still use `whereEqualTo` when the name of the category is now the collection itself? That's one of the reasons why you got the name of the collections out of that Map, so you don't use such a call, right? – Alex Mamo Jul 23 '20 at 09:14
  • Hi @Alex, I tried your solution, but I get same result as "whereArrayContainsAny", also I cannot make query like orderBy and limit.. (as I'm using pagination) please see my updated question in second attempt section. – Walid Jul 23 '20 at 15:22
  • @Walid Instead of describing how your database looks like, please add a screenshot of it. – Alex Mamo Jul 23 '20 at 18:42
  • It becomes complicated right now. I recommend you divide this problem into smaller ones. However, the entire idea behind this approach is to add actual documents in those collections so you can get them very simple. – Alex Mamo Jul 24 '20 at 10:33
  • yes I understand what you mean, but with that approach, I cannot do it with pagination.. I just wanted, if I select meal_1 and meal_2 I get list of docs that contains both filters.. – Walid Jul 24 '20 at 10:48
  • Yes, you can. You can get the results from the first and second query and add to `whenAllSuccess(firstTask, secondTask)`. You'll get a combined List of documents that can be paginated. – Alex Mamo Jul 24 '20 at 10:51
  • Pretty much like I did. but in this case, I will not need anymore the collection that hold all the docs, as I will save them across the filter collections. I will try this now! Thank you Alex! – Walid Jul 24 '20 at 10:56
  • Is there everything ok, have you solved the problem? – Alex Mamo Jul 27 '20 at 07:19
  • Unfortunately no, I did what you said, with whenAllSuccess(firstTask, secondTask), but I'm getting same result as whenArrayContainsAny. I got all docs from firstTask and secondTask, and I had to do a filter in the client to keep only docs that have both filters. in other word docs that exists in both filter_collection_1 and filter_collection_2. and it does not work with pagination as Im getting docs from several collections at same time. – Walid Jul 27 '20 at 08:10
  • Hi @AlexMamo, one user asked about structuring the database as a n-ary tree. I think he refers to data denormalization, so maybe you can answer his question with the answer to have made here. https://stackoverflow.com/questions/63424992/firestore-nosql-database-structure-as-a-n-ary-tree?r=SearchResults – Victor Molina Aug 15 '20 at 15:56
  • @Victor Hi Victor. I'm not sure if the exact same issue but you can add this link there if you want. – Alex Mamo Aug 15 '20 at 16:00