Server side filtering of Firebase data - ios

I have a Firebase db structure as follows:
{
"listings" : {
"-KOt8OUGkUphoMyqEXJ2" : {
"created" : 1470911323208,
"ends" : 1470911323209,
"make" : "LONDON TAXIS INT",
"model" : "TX4 BRONZE",
"status" : "For Sale",
},
"-KOt97_P8sJW7woED4aH" : {
"created" : 1470911515115,
"ends" : 1471775515000,
"make" : "NISSAN",
"model" : "QASHQAI N-TEC",
"status" : "For Sale",
},
"-KOt_BYYUEaXu_LNvnUv" : {
"created" : 1470918609414,
"ends" : 1471782609000,
"make" : "MAZDA",
"model" : "3 TS",
"status" : "For Sale",
}
}
}
I use GeoFire to get keys for listings in a given radius. I then use observeSingleEventOfType to return the listing for the key. This all works fine.
However, I would like to only return a listing if the "ends" timestamp > current time. I have tried the following approach:
geoQueryHandle = geoQuery.observeEventType(.KeyEntered, withBlock: {(key, location) in
let listingRef = self.ref.child("listings").child(key)
let now = Int(NSDate().timeIntervalSince1970 * 1000)
let query = listingRef.queryStartingAtValue(now, childKey: "ends")
query.observeSingleEventOfType(.Value, withBlock: {snapshot in
if let listing = Listing(snapshot: snapshot, location: location) {
//...populate my tableview
}
}
})
Can anyone advise me on why my query isn't working?
Thanks

Firebase queries apply ordering/filtering to the properties of the child nodes of the location where you execute them.
queryLocation
child1
filterProperty: filterValue
child2
filterProperty: filterValue
Since your listingRef already points to a specific listing, the order/filter you add will apply to child nodes one level deeper.
The simplest way to get your use-case working seems to simply filter the node client-side:
let listingRef = self.ref.child("listings").child(key)
query.observeSingleEventOfType(.Value, withBlock: {snapshot in
if let listing = Listing(snapshot: snapshot, location: location) {
let now = Int(NSDate().timeIntervalSince1970 * 1000)
// TODO: exit if snapshot.child("ends").getValue() < now
//...populate my tableview
}
}
A more complex, but more efficient, way to accomplish this would be to remove the expired events from the Geofire location.

Related

IndexOn on firebase Root to query entire database swift 3

I am trying to query by name inside firebase database, I want to return all the names matching the query. But I can't seem to get it to work, I am getting error Consider adding ".indexOn": "name" at / to your security rules.
Security Rules:
{
"rules": {
".read": "true",
".write": "true",
"goals_new": {
".indexOn": ["name"]
}
}
}
I Can retrieve the names for specific child like this:
let query = ref.child("goals_new").queryOrdered(byChild:"name").queryEqual(toValue: name)
query.observe(.childAdded) { (snapshot) in
// if let values =
if let values = snapshot.value as? [String:String] {
print(values)
print(values["name"]?.count ?? "")
}
}
But I want to retrieve all the names in the database that matches the query
Database Structure:
"goals_new" : [ null, {
"name" : "Eric thomas",
"pic" : “…….”,
"title" : "Be Obsessed with your Goals",
"url" : “……”
},
{
"name" : "Bob Proctor",
"pic" : “……….",
"title" : "Goal Achievement System",
"url" : “………”
},
Any help would be appreciated.
Consider adding ".indexOn": "name" at / to your security rules <- Is only a warning so you don't have to worry much about that.
I'm seeing perhaps an error. Looks like you're using an array to store your objects of 'goals_news' because of the keys are numbers 1, 2, etc... Maybe that's is messing with your query, you have to change them for string keys like every time you add one use firebase method childByAutoID().
Better use .value instead of .childAdded method so you can get all the objects that matches the name, something like this:
let query = ref.child("goals_new").queryOrdered(byChild:"name").queryEqual(toValue: name)
query.observe(.value) { (snapshot) in
guard snapshot.exists() && snapshot.hasChildren() else {return}
for snap in snapshot.children {
var currentSnapValue = (snap as! DataSnapshot).value as! [String: String]
print("Name: \(currentSnapValue["name"])")
}
}
That way you're getting all the objects that matches the name

Dealing with memory usage due to high element count in Firebase query

I'm writing code that is appending elements from firebase to an array to perform a simple search using a textfield.
The code for the method is below:
var filteredEvents = [Event]()
var eventsArray = [Event]()
fileprivate func fetchEvents(){
print("Fetching events....")
//create a reference to the location in the database that you want to pull from and observe the value there
let ref = Database.database().reference().child("events")
// this will retur a snapshot with all the data at that location in the database and cast the results as a dictionary for later use
ref.observe(.value, with: { (snapshot) in
guard let dictionaries = snapshot.value as? [String: Any] else{
return
}
//does the job of sorting dictionary elements by key and value
//displaying the key and each corresponding value
dictionaries.forEach({ (key,value) in
// print(key, value)
//creating an eventDictionary to store the results of previous call
guard let eventDictionary = value as? [String: Any] else{
return
}
//will cast each of the values as an Event based off my included struct
//Make sure to create a model it is the only way to have the data in the format you want for easy access
let events = Event(currentEventKey: key, dictionary:eventDictionary)
// appends that to the dictionary to create the dictionary of events
self.eventsArray.append(events)
})
// will sort the array elements based off the name
self.eventsArray.sort(by: { (event1, event2) -> Bool in
return event1.currentEventName.compare(event2.currentEventName) == .orderedAscending
})
// will again reload the data
self.collectionView?.reloadData()
}) { (err) in
print("Failed to fetch events for search")
}
}
I personally did not think of the chance where I may have a great number of events. I won't be able to just append all 1000+ events to a dictionary. That will ratchet my memory up. Is there anyway I could have the query respond to the text field. Also could anyone maybe help me with the line for the query that would perform this action but not destroy my memory?
"events" : {
"CCDS" : {
"attend:count" : 1,
"event:date" : {
"end:date" : "08/09/2017",
"end:time" : "7:00 PM",
"start:date" : "08/09/2017",
"start:time" : "5:00 PM"
},
"event:description" : "Happy hour is more joyful in the summer thanks to Center City District Sips, which offers discounted drinks and appetizers every Wednesday evening. Catch up with old friends and make a few new ones as Center City’s best bars and restaurants host the summer’s happiest hour every Wednesday from 5-7 p.m. Enjoy $5 cocktails, $4 wine, $3 beers and half-price appetizers at dozens and dozens of bars and restaurants.",
"event:imageURL" :someURL",
"event:location" : {
"event:city" : "Philadelphia",
"event:state" : "PA",
"event:street:address" : "660 Chestnut St",
"event:zip" : 19106
},
"event:name" : "Center City District Sips"
},
"MIA" : {
"attend:count" : 1,
"event:date" : {
"end:date" : "09/03/2017",
"end:time" : "7:00 PM",
"start:date" : "09/02/2017",
"start:time" : "12:00 PM"
},
"event:description" : "Budweiser Made in America Festival is an annual music festival held in Philadelphia and formerly simultaneously held in Los Angeles.Sponsored by Anheuser–Busch and produced by Live Nation, the event features several stages that continuously host live music from a wide range of genres including hip hop, rock, pop, R&B, and EDM.",
"event:imageURL" : "someURL",
"event:location" : {
"event:city" : "Philadelphia",
"event:state" : "PA",
"event:street:address" : "Ben Franklin Parkway",
"event:zip" : 19130
},
"event:name" : "Made In America"
}
},
For example I want to pull all information about an event that I have searched for using the query. So if I begin to type in Made in America it will pull all relevant information from the events tab about that event
This is what I currently have
fileprivate func fetchEvents(searchString: String){
print("Fetching events....")
//create a reference to the location in the database that you want to pull from and observe the value there
let ref = Database.database().reference().child("events")
// this will retur a snapshot with all the data at that location in the database and cast the results as a dictionary for later use
let query = ref.queryOrdered(byChild: "event:name").queryEqual(toValue: searchString)
print(query)
query.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else{
print(snapshot.value)
return
}
print(snapshot.value)
}) { (err) in
print("Failed to fetch event data", err)
}
}
Returns this
(/events { ep = Made In America; i = "event:name"; sp = Made In America; })
It appears the question is
"How can I query for a value contained in a child node?"
Given a structure similar to the original
"events" : {
"CCDS" : {
"attend:count" : 1,
"event:imageURL" :"someURL",
"event:name" : "Center City District Sips"
"MIA" : {
"attend:count" : 1,
"event:imageURL" : "someURL",
"event:name" : "Made In America"
a Firebase query would return the node you want.
If the user typed in Made In America and tapped the search button here's the query to return that node in a snapshot.
let searchString = "Made In America"
let ref = self.ref.child("events")
let query = ref.queryOrdered(byChild: "event:name").queryEqualTo(searchString)
query.observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let snap = child as! DataSnapshot
let eventDict = snap.value as! [String: Any]
let attendCount = eventDict["attend:count"] as! String
let url = eventDict["event:imageURL"} as! String
}
})
If you want to do a partial string match, where the user could type in just a few characters like Made the code is similar but you need to let firebase return all of the matches starting with Made...
The query would look like this
let startString = "Made"
let endString = "Made" + "\\uf8ff"
let query = ref.queryOrdered(byChild: "event:name")
.queryStarting(atValue: startString)
.queryEnding(atValue: endString")
The "\uf8ff" is a character at a very high code level in Unicode - because of that it encompasses all of the preceeding characters.
However, querying 'on the fly' can create an unresponsive or sluggish UI so it's not recommended.
An alternative is to create a seperate node that contains a lot less info and contains the elements the user would search for and a reference to the event node.
So the main node containing all the data looks like this
"events" : {
"-uyuh8s8j8jsdas" : {
"event": "CCDS"
"attend:count" : 1,
"event:imageURL" : "someURL",
"-y88jsijsijjids" : {
"event": "MIA"
"attend:count" : 1,
"event:imageURL" : "someURL",
and a 'smaller' node would look like this
events_for_searching
-uyuh8s8j8jsdas
event:name: "Center City District Sips"
-y88jsijsijjids
event:name: "Made In America"
With this, you could load all of the nodes from the events_for_searching into an array (then filter the array as the user types) which would make the UI very responsive and when the user selects a name, you can then use the key from that node as a reference to load the data from the events node via an observeSingleEvent function.
EDIT
In response to a comment, I wanted to add a bit more detail in code.
Here's my structure
"events" : {
"event_0" : {
"event:name" : "An Event"
},
"event_1" : {
"event:name" : "Made In America"
}
},
and the code to query for event:name: Made In America
let searchString = "Made In America"
let ref = self.ref.child("events")
let query = ref.queryOrdered(byChild: "event:name").queryEqual(toValue: searchString)
query.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else{
print(snapshot.value)
return
}
print(snapshot.value)
}) { (err) in
print("Failed to fetch event data", err)
}
and the output
Optional({
"event_1" = {
"event:name" = "Made In America";
};
})

Firebase queryOrderedByChild() method not giving sorted data

My database structure is some thing like this:
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"score": 4,
},
"ghopper": { ... },
"eclarke": { ... }
}
}
I am trying to retrieve top 20 scores in descending order.
let queryRef = FIRDatabase.database().reference().child("users").queryOrderedByChild("score").queryLimitedToLast(20)
queryRef.observeSingleEventOfType(.Value, withBlock: { (querySnapShot) in
print(querySnapShot.value)
})
i am trying to get output like
score": 4
score": 3
score": 2
or
score": 2
score": 3
score": 4
or
2
3
4
Please let me know how to solve this.
When you request the children in a specific order, the resulting snapshot will contain both the data that matches the query and information about the order in which you requested them.
But when you request the .value of the snapshot, the keys+data are converted to a Dictionary<String,AnyObject>. Since a dictionary does not have an extra place to put the information about the order, that information is lost when converting to a dictionary.
The solution is to not convert to a dictionary prematurely and instead loop over the snapshot:
queryRef.observeSingleEventOfType(.Value, withBlock: { (querySnapShot) in
for childSnapshot in querySnapShot.children {
print(childSnapshot.value)
}
})
You can also listen to the .ChildAdded event, instead of .Value, in which case the children will arrive in the correct value:
queryRef.observeSingleEventOfType(.ChildAdded, withBlock: { (childSnapshot) in
print(childSnapshot.value)
})
Update
I just added this JSON to my database:
{
"users" : {
"alovelace" : {
"name" : "Ada Lovelace",
"score" : 4
},
"eclarke" : {
"name" : "Emily Clarke",
"score" : 5
},
"ghopper" : {
"name" : "Grace Hopper",
"score" : 2
}
}
}
And then ran this code:
let queryRef = ref.child("users").queryOrderedByChild("score").queryLimitedToLast(20);
queryRef.observeEventType(.ChildAdded) { (snapshot) in
print(snapshot.key)
}
The output is:
ghopper
alovelace
eclarke
Which is the users in ascending order of score.
Update to add more on getting the scores in descending order
The above code gets the 20 highest scores in ascending order. There is no API call to return themthem in descending score.
But reversing 20 items client side is no performance concern, you just need to write the code for it. See for example this answer.
If you really are stuck on reversing them client side, you can add an inverted score. See this answer for an example of that.
Use method observeEventType instead of observeSingleEventOfType.
Also, make FIRDataEventType to ChildAdded.
Last, If you want Top 20 items, use queryLimitedToFirst instead of queryLimitedToLast.
{
"users" : {
"alovelace" : {
"name" : "Ada Lovelace",
"score" : 4
},
"eclarke" : {
"name" : "Emily Clarke",
"score" : 5
},
"ghopper" : {
"name" : "Grace Hopper",
"score" : 2
}
}
}
For the dataset above
let queryRef = FIRDatabase.database().reference().child("users").queryOrderedByChild("score").queryLimitedToFirst(20)
queryRef.observeEventType(.ChildAdded, withBlock: { (snapshot) in
print("key: \(snapshot.key), value: \(snapshot.value)")
})
key: ghopper, value: Optional({
name = Grace Hopper;
score = 2;
})
key: alovelace, value: Optional({
name = Ada Lovelace;
score = 4;
})
key: eclarke, value: Optional({
name = Emily Clarke;
score = 5;
})
Snapshot will returns the contents as native types.
Data types returned:
NSDictionary
NSArray
NSNumber (also includes booleans)
NSString
So, you can get your scores this way.
let queryRef = FIRDatabase.database().reference().child("users").queryOrderedByChild("score").queryLimitedToFirst(20)
queryRef.observeEventType(.ChildAdded, withBlock: { (snapshot) in
if let scores = snapshot.value as? NSDictionary {
print(scores["score"])
}
})
Optional(2)
Optional(4)
Optional(5)
Moreover, the default of realtime database return everything in ascending order.
If you want descending order, you can make some tricks(4:40) in your database.

Firebase - How can I retrieve the objects in a date range?

I have the next JSON structure in Firebase:
{
"-KGX6kYg1NO6d9hn-8um" : {
"phase" : "A",
"timestamp" : "12-18-2015 19:43:37"
},
"-KGXOGSxa3vompZX9UO_" : {
"phase" : "B",
"timestamp" : "03-28-2016 15:28:21"
},
"-KMUvszD-vm3Nu02sofd" : {
"phase" : "A",
"timestamp" : "04-03-2014 03:57:56"
}
}
Is it possible to filter the objects by the timestamp key through a range of date?.. For example, I want to get the objects with timestamp from January 2015 to today date. If not possible, what's the better way to filter the objects by dates?... I'm developing an iOS app.
Thanks.
You can sort the snapshot data by timestamp and define a timestamp limit from which you want your data.
For example you want all data from a specific timestamp timestamp1, your reference handler should look like:
let refHandle = tableRef.queryOrderedByChild("timestamp").queryEndingAtValue("timestamp1").observeEventType(FIRDataEventType.Value, withBlock: { (snapshot) in
for item in snapshot.children {
}
})
You can also apply number of records that you want by adding queryLimitedToLast or queryLimitedToFirst like :
let refHandle = tableRef.queryOrderedByChild("timestamp").queryEndingAtValue("some_time_stamp").queryLimitedToLast(kPostLimit + 1).observeEventType(FIRDataEventType.Value, withBlock: { (snapshot) in
for item in snapshot.children {
}
})
Also, you want to have a look at the following post about common sql queries in Firebase.

Firebase in Swift nested query not working properly

I have a JSON structure like the following:
{
"groups" : {
"-KAv867tzVgIghmr15CM" : {
"author" : "ruben",
"name" : "Item A"
},
"-KAv87nqLEG1Jtc04Ebn" : {
"author" : "ruben",
"name" : "Item B"
},
"-KAv88yZe8KTfkjAE7In" : {
"author" : "ruben",
"name" : "Item C"
}
},
"users" : {
"rsenov : {
"avatar" : "guest",
"email" : "ruben#ruben.com",
"groups" : {
"-KAv867tzVgIghmr15CM" : "true",
"-KAv87nqLEG1Jtc04Ebn" : "true",
"-KAv88yZe8KTfkjAE7In" : "true"
}
}
}
}
Every user has the element "groups" with a childByAutoId() key. Then I have the list of all the groups that exists in the app.
Every time that I run the app, I get the current user logged url reference, and I get the list of the groups of that user (in this case, the logged in user is "rsenov" that has 3 groups).
For every group that this user belongs to, I iterate through the groups url reference, looking for getting the information of that 3 groups.
I do this like this:
func loadTable() {
self.groups = []
var counter = 0
self.meses = []
var tempItems = [String]()
DataService.dataService.CURRENT_USER_GROUPS_REF.observeEventType(.Value, withBlock: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FDataSnapshot] {
tempItems = []
for snap in snapshots {
DataService.dataService.GROUPS_REF.childByAppendingPath(snap.key).queryOrderedByChild("name").observeEventType(.Value, withBlock: { snapshot in
if let postDictionary = snapshot.value as? Dictionary<String, AnyObject> {
tempItems.append(snapshot.value.objectForKey("name") as! String)
let key = snapshot.key
let group = Group(key: key, dictionary: postDictionary)
self.groups.insert(group, atIndex: 0)
}
counter++
if (counter == snapshots.count) {
self.meses = tempItems
self.miTabla.reloadData()
}
})
}
}
})
}
I think this is not a good idea of iterating in that way. For example, if there is a change of some child in the GROUPS_REF url, the code only runs in that nested code, and since it doesn't have the "snap.key" value got from the for loop, it doesn't work.
Which is the best way to do a good query in this case?
Phew, that took some time to write. Mostly because I don't iOS/Swift a lot:
let ref = Firebase(url: "https://stackoverflow.firebaseio.com/35514497")
let CURRENT_USER_GROUPS_REF = ref.childByAppendingPath("users/rsenov/groups")
let GROUPS_REF = ref.childByAppendingPath("groups")
var counter: UInt = 0
var groupNames = [String]()
CURRENT_USER_GROUPS_REF.observeEventType(.Value, withBlock: { groupKeys in
for groupKey in groupKeys.children {
print("Loading group \(groupKey.key)")
GROUPS_REF.childByAppendingPath(groupKey.key).observeSingleEventOfType(.Value, withBlock: { snapshot in
print(snapshot.value)
if (snapshot.childSnapshotForPath("name").exists()) {
groupNames.append(snapshot.value.objectForKey("name") as! String)
}
counter++
if (counter == groupKeys.childrenCount) {
print(groupNames)
}
})
}
})
By the way, this is how you create a minimal, complete verifiable example. The code has no external dependencies (such as Group and DataService in your code) and only contains what's relevant to the answer.
The important bits:
I used observeSingleEventOfType to get each group, since I don't want to get more callbacks if a group changes
I use snapshot.childSnapshotForPath("name").exists() to check if your group has a name. You probably want to either ensure they all have names or add them to the list with some other property in the real app.
Frank's answer is on-point. I wanted to throw in an alternative that may or may not work for your situation as it requires a slight alteration to the database.
groups
gid_0
author: "ruben"
name: "Item A"
users
uid_0: true
gid_1
author: "ruben"
name: "Item B"
users
uid_1: true
gid_2
author: "ruben"
name: "Item C"
users
uid_0: true
And then some ObjC Code for a Deep Query
Firebase *ref = [self.myRootRef childByAppendingPath:#"groups"];
FQuery *query1 = [ref queryOrderedByChild:#"users/uid_0"];
FQuery *query2 = [query1 queryEqualToValue:#"true"];
[query2 observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"key: %# value: %#", snapshot.key, snapshot.value);
}];
This code does a deep query on the /groups for all groups that have a /users/uid_0 = true. In this case it returns gid_0 and gid_2
It eliminates the need for iterations and multiple calls to the database.
Adding a /users/ node to each group with a list of the uid's may offer some additional flexibility.
Just a thought.

Resources