Skip to content

Understanding search in Chat SDK

bensmiley edited this page Mar 24, 2017 · 3 revisions

If you have used Firebase before you will know that the database is in JSON format instead of an SQL database. This means that querying data is performed in a different way.

Classic Search:

When searching with an SQL database we would normally search using a query. Limits would be placed on the query before returning all the data which conforms to it. For example you can see how CoreData stores its objects in tables and how Parse used to query its tabulated database:

PFQuery * query = [PFUser query];
[query whereKey:@"entityID" equalTo:user.entityID];
query.limit = 1;
    
[query findObjectsInBackgroundWithBlock:^(NSArray * objects, NSError * error) {
        
}];

CoreData SQL data structure

Firebase Search

Firebase stores its data in JSON format. You can imagine it like a tree. The individual branches are the specific objects contained in the project, the smaller branches attached to those are the details specific to those objects. The more specific we get the further along each branch we go.

Firebase data structure

This data structure is very efficient for an instant messaging service because it enables us to:

  • Observe certain paths for changes
  • Quickly find specific data
  • Easily add, modify or remove specific data

The disadvantage is this data structure isn't very efficient for searching. This is due to not being able to search a tree structure as efficiently as searching specific tables - instead of being able to search a single table we have to traverse to the ends of lots of branches. Because of this we need to modify our database to ensure that we can search for specific users quickly and accurately.

SearchIndex

To search more efficiently we create a searchIndex for specific areas of data. This means we only need to perform our search on a specific subset of data instead of needing to search the entire database. This area contains all the information which users are able to search for (name, email etc).

Note: This makes it important to keep this data updated with the users latest information

Below you can see an example of storing specific user information. Each user has their details added, if these details match our search criteria we can return the user's entityID to display the user.

searchIndex {
    user1EntityID {
        email: example@email.com
        facebookID: 1234567890
        name: user1
    }
    user2EntityID {
        email: email@email.com
        facebookID: 9876543210
        name: user2
    }
}

Any value we might want to search for can be added to this specific area of the database.

Security Rules

Before we look at the search code it is worth mentioning that some modifications are needed to the project security rules to improve the search functionality.

**Note:**To access your security rules navigate to your Firebase app dashboard -> database -> rules

You will need to add a variation of the code below:

"searchIndex": {

    // Any authenticated user can read the data
    ".read": "root.child($root+'/users/'+auth.uid).exists()",

    // Index the items to improve search performance 
    ".indexOn": ["name", "email", "phone"],

    "$user_id": {
      
      // We can only write to the index related to our user
      ".write": "$user_id == auth.uid"  
    }
},

We have added rules specifically for the searchIndex.

  • Users can only read the data if they have an authenticated unique ID
  • We add an indexOn field to help Firebase know we will be searching for these specific values. This will increase the search speed
  • Users can only write data to their own searchIndex

Note: If you are unsure on how to add these security rules you can checkout the rules.json file included in the Chat SDK download or get in touch and we'll help you out

Search Flow

When designing your app you need to decide which entries of a user's profile (or details of a chat thread if you want to include chats search) need to be searchable. We can then update the searchIndex when the object is updated.

Chat SDK already has user search implemented so I will be using example for this, thread search would be dealt with in a similar manner.

Note: The Chat SDK is specifically referenced in the project for quick access. You can then set your specific JSON dictionary to this path to set your value.

-(RXPromise *) updateIndexForUser: (id<PUser>) userModel {
    
    RXPromise * promise = [RXPromise new];
    
    // 1.
    // We use the searchIndex reference to immediately access the searchIndex path
    FIRDatabaseReference * ref = [[FIRDatabaseReference searchIndexRef] child:[BNetworkManager sharedManager].a.auth.currentUserEntityID];
    
    // 2.
    NSString * email = [userModel metaStringForKey:bEmailKey];
    NSString * phone = [userModel metaStringForKey:bPhoneKey];
    
    NSDictionary * value = @{bNameKey: userModel.name ? [self processForQuery:userModel.name] : @"",
                         bEmailKey: email ? [self processForQuery:email] : @"",
                         bPhoneKey: phone ? [self processForQuery:phone] : @""};
    
    // The search index works like: /searchIndex/[user entity id]/user details
    // 3.
    [ref setValue:value withCompletionBlock:^(NSError * error, FIRDatabaseReference * firebase) {
        if (!error) {
            [promise resolveWithResult:Nil];
        }
        else {
            [promise rejectWithReason:error];
        }
    }];
    
    return promise;
}
  1. First we get the Firebase reference for the searchIndex. We add the user's entityID onto this to ensure we are setting the current user's details.
  2. Next we get the users current details. These are saved in the user area of the database as meta data. This ensures the searchIndex has the most up to date values.
  3. Finally we set the Firebase reference with our dictionary value. This updates the user searchIndex area.

We call this code from the user's profile page in the ChatSDK meaning we refresh the values if they change their profile information.

[[BNetworkManager sharedManager].a.search updateIndexForUser:user].thenOnMain(Nil, ^id(NSError * error) {
    [UIView alertWithTitle:[NSBundle t:bErrorTitle] withError:error];
    return error;
});

Searching for users

Now we are ready to look at how we search for users in the ChatSDK

Note: You will notice two similar search functions in ChatSDK. usersForIndexes and usersForIndex. We user the latter to search multiple indexes at the same time.

If we look at the search function in BFirebaseSearchHandler.m

-(RXPromise *) usersForIndex: (NSString *) index withValue: (NSString *) value limit: (int) limit userAdded: (void(^)(id<PUser> user)) userAdded

First we make the query:

FIRDatabaseQuery * query = [[[[FIRDatabaseReference searchIndexRef] queryOrderedByChild:index] queryStartingAtValue:value] queryLimitedToFirst:limit];

This is querying the entire searchIndex:

  • Ordering by the index (name, phone etc) that we are searching with.
  • Starting at the value we have entered (this might be the first few letters of a name)
  • Only return a certain number of results

We then run the query which returns all the possible results:

[query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * snapshot) {

}];

Inside this search query we now check the limited number of results returned for their accuracy.

resultValue = dict[key][index];
if(resultValue) {
    // Transform the result value to lower case / no spaces
    resultValue = [self processForQuery:resultValue];
                    
    // If the query is longer than the result then it's obviously not a match
    if (resultValue.length < value.length) {
        continue;
    }
                    
    // Trim it to the length of the input query
    resultValue = [resultValue substringToIndex:value.length];
                    
    // If they match add it to the result
    if ([value isEqualToString:resultValue]) {
        [validUIDs addObject:key];
    }
}

This leaves us with an array of user entityIDs who match the search query performed.

We finish by looping over these entityIDs and adding the users to our device. For us to be able to display the users we need to have all their details to display. We do this with the [user once] function which downloads the user information once - without adding a listener to look for changes.

// Loop over the IDs and get the users
for(NSString * entityID in validUIDs) {
                    
    CCUserWrapper * user = [CCUserWrapper userWithEntityID:entityID];
    [userPromises addObject:[user once].thenOnMain(^id(id<PUserWrapper> u) {
                        
         if (u.model.name.length) {
             userAdded(user.model);
         }
         return Nil;
    }, Nil)];
}

We now return the user object to the search completion block. We can then add this user to a searchArray and display the user's search information.

When search for users with Firebase we are only interested in returning users which fulfil our criteria. Firebase performs a search query and then returns potential user entityIDs. We then perform some more filtering to remove any users which do not fulfil our criteria. This leaves us with an array of user entityIDs which we can then use to create user objects in CoreData and return in the search tableView.

Note: The userAdded completion block will return each user once it has been added to CoreData. You can then add the users as they are returned and refresh the tableView to update the newly searched users.

To conclude Firebase has a very powerful search functionality which the Chat SDK uses to return users based on name, email and phone-number. The search has been written to enable you to quickly scale up your searches meaning you can return multiple users for multiple searches extremely quickly. It could also be easily transferable to other objects in the Chat SDK. If you want to search for specific chats you would simply need to make a chatSearchIndex, update the chat's info when it is created/edited and then created a similar search function to search and return your thread entityIDs.

Chat SDK User Search