1

I'm using a UISearchController and I want to query 2 nodes down from root on a comment key. It would be root > posts > uid > postId > comment

root
  |
  @--posts
       |
       @--uid123
            |
            @--postId987
                  |
                  |-comment: "I like pizza"

I tried chaining multiple queries let commentRef = postsRef.queryOrderedByKey().queryOrdered(byChild: "comment").queryStarting(atValue: searchText).queryEnding(atValue: searchText+"\u{f8ff}" but I got a crash:

*** Terminating app due to uncaught exception 'InvalidQueryParameter', reason: 'Cannot use multiple queryOrderedBy calls!'

What's the best way to query several levels down?

Code that caused crash:

func updateSearchResults(for searchController: UISearchController) {

    guard let searchText = searchController.searchBar.text?.lowercased() else { return }

    let postsRef = Database.database().reference().child("posts")

    let commentRef = postsRef.queryOrderedByKey().queryOrdered(byChild: "comment").queryStarting(atValue: searchText).queryEnding(atValue: searchText+"\u{f8ff}")

    commentRef.observeSingleEvent(of: .value, with: { (snapshot) in

        guard let dictionaries = snapshot.value as? [String: Any] else { return }

        dictionaries.forEach({ (key, value) in

            guard let dict = value as? [String: Any] else { return }
            let post = Post(dict: dict)

            let isContained = self.filteredComments.contains(where: { (containedPost) -> Bool in
                return post.comment == containedPost.comment
            })
            if !isContained {
                self.filteredComments.append(post)
                self.collectionView?.reloadData()
            }
        })
    })
}

I found another way to do it below. I first get the .value on the posts ref, loop through all the children, then I attach each snapshot.key as a child to the postsRef and get the comment from there. It actually works. The problem is it doesn't seem very efficient especially if there are millions of posts and/or users to sort through.

let postsRef = Database.database().reference().child("posts")

postsRef.observeSingleEvent(of: .value) { (snapshot) in

    for child in snapshot.children {

        let uid = child as! DataSnapshot

        let commentRef = postsRef.child(uid.key).queryOrdered(byChild: "comment").queryStarting(atValue: searchText).queryEnding(atValue: searchText+"\u{f8ff}")

        commentRef.observeSingleEvent(of: .value, with: { (snapshot) in
            // the rest of the code
    }
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • You can use Deep Path Queries if your nodes have a *static path* (you can't do that with nodes that contain dynamic paths). The real problem here is how you've structured you data. It needs to be denormalized and by doing that will make your queries super easy. It's also not really clear what you are query-ing; are you looking for all comments made by user123 that contain *I like pizza*? If so, then your query isn't correct. – Jay Aug 04 '18 at 13:37
  • @Jay sorry if the question wasn’t clear. Posts contains all posts by all users, each user’s post has a postId, each postId has a k/v pair. I want to run a query on any post that has a comment with value matching I like pizza – Lance Samaria Aug 04 '18 at 13:52
  • Oh - super easy to fix! Just change your structure and/or add another node (again, denormalizing makes this a snap). A parent node of post_comments and then child nodes of postId/user_id: "uid123" and postId/comment: "I like pizza". Then you can do that query easily! Remember: Firebase structures are based on what you want to get out of it and [Denormalizing is Normal](https://firebase.googleblog.com/2013/04/denormalizing-your-data-is-normal.html) – Jay Aug 04 '18 at 13:59
  • @Jay thanks, I’m not on my cpu at the moment to try it out. I’ll look into later and respond back. Appreciate the help! – Lance Samaria Aug 04 '18 at 14:01
  • I threw out an answer to help with denormalization. Take a look. – Jay Aug 04 '18 at 14:03

2 Answers2

0

According to this answer, when you get to the final node you want to query on, Firebase can only query 1 level deep, querying in multiple deep paths with dynamic nodes (nodes created by childByAutoId) isn’t supported.

Example you can’t query root > posts > postId > userId > whatever your looking for because postId and userId are both dynamic.

Seems the only way to achieve what I asked is to duplicate the data and search it at that duplicated path.

root > posts > postId > whatever your looking for

enter image description here

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • I think you may have not read that linked answer completely. You can definitely query deeper than one level, you just can't do it with *dynamic paths*. Please see my answer to [this question](https://stackoverflow.com/questions/41428555/how-to-query-firebase-database-object-when-criteria-property-is-nested/41431930#41431930) and also read the post from Firebase about [Deep Path Queries](https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html). You can also just search here on SO for Deep Path Query or Deep Query. – Jay Aug 04 '18 at 13:34
  • @Jay hey thanks for looking into the q&a. I just looked at your answer and the 2nd part of it is similar to what I was asking about. The difference I see is that “queryOrdered(byChild: “users/user_1”)” user_1 is a static key so you can always get to it’s children because it can always be referenced in a path. In my situation I’m using childByAutoId to create the key so how would I be able to create a path like that because it can’t be referenced. – Lance Samaria Aug 04 '18 at 13:46
  • You answered the it in your comment under the question. It can’t be done. – Lance Samaria Aug 04 '18 at 13:53
0

I am going to throw this out as a possible solution.

It's not exactly clear why the current structure is what it is, but I would suggest changing it to

post_comments
   post_id_x
      posted_by: "uid1234"
      comment: "I like pizza"

if the structure needs to stay as is, then simply add this as another node which will make your comment queries much simpler.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • I actually followed a tutorial and in it the person said the structure should be as follows. There is a users node and a posts node. USERS_NODE: users > userId > postId and POSTS_NODE: posts > userId > postId. What your saying is there isn’t a need to add the userId under the posts node and instead it should be included as a k/v pair inside the postId POSTS_NODE: posts > postId > k/v – Lance Samaria Aug 04 '18 at 14:12
  • Actually, the way you have it is the exact way I setup an additional node to search on. I currently have 2 nodes. POSTS_NODE: posts > userId > postId > k/v pairs and SEARCH_POSTS: searchPosts > postId > k/v pairs. The difference is the k/v at the searchPosts node just has the comments and userId where the k/v at the other posts contain the comment, date, and other meta data. I basically duplicated the data at the searchPosts node from the postsNode. As I look at it it doesn’t make much sense to duplicate it. – Lance Samaria Aug 04 '18 at 14:17
  • Btw I created the searchPosts node after I asked this question and after I found out I couldn’t query on multiple dynamic paths. – Lance Samaria Aug 04 '18 at 14:26