DEV Community

Sean Perkins for TeamHive

Posted on • Edited on

Using Algolia With Firebase Angular Apps

What is Algolia?

Algolia is a super powerful, scalable API service that allows developers to send different forms of data into their platform and quickly perform search, sort and complex filter queries on top of it. The service is incredibly fast, by using replica indexes to pre-build common query conditions to send your data back as quick as possible.

Why use Algolia with Firebase?

Firebase has come a long way in terms of its accessibility with querying data structures, especially in Firestore. Even with these advancements, it has limitations and often time requires pre-sorted data, using Firebase’s syntax sugar with push ids (push ids contain a date hash in their generation) and sacrificing extra reads/writes and straight forward object structure. Firebase also officially recommends Algolia for performing full-text search operations in Firestore.

Getting Started

In this working example, we will be using Firebase Cloud Functions with triggers to help assist with syncing data changes from Firestore over to Algolia. We will also be using the Algolia Node.JS and JavaScript client module for interacting with their service.

Firebase Cloud Functions

In your functions directory you will need to install the following dependencies to leverage Algolia.

npm install --save algoliasearch @types/algoliasearch
Enter fullscreen mode Exit fullscreen mode

For this example we will listen for whenever a new user document is created, updated or deleted in our custom Firestore collection “users”.

For each of the below examples you will need to replace appId and apiKey with your own access tokens generated through Algolia’s admin panel.

user.onCreate.ts

The userOnCreate trigger is dispatched every time a new document is created in the users collection. In the example below we initialize Algolia with our app’s id and unique API key and initialize the index we want to use in Algolia. Algolia recommends naming your index by the instance/environment you are working with (i.e. dev_, prod_, staging_, next_).

We are also replicating to indexes so that we can sort by the user’s name in either ascending or descending order. Algolia reserves objectID for correlating records in their world; we will use the new document’s path id.

import * as algoliasearch from 'algoliasearch';
import * as functions from 'firebase-functions';

export const userOnCreate = functions.firestore
   .document('users/{id}')
   .onCreate(async (change, context) => {
       const user = change.data();
       const client = algoliasearch('appId', 'apiKey');
       const index = client.initIndex('dev_users');
       await index.setSettings({
           replicas: [
               'dev_users_name_desc',
               'dev_users_name_asc'
           ]
       });
       return index.addObject({
           objectID: change.id,
           ...user
       });
   });
Enter fullscreen mode Exit fullscreen mode

user.onUpdate.ts

The userOnUpdate trigger is very similar to the create trigger. The difference is that we do not need to re-specify the replica indexes since once we register them; they will automatically push data over to the replica indexes any time we write to the parent index (dev_users).

To reduce the operation cost, Algolia allows partial updates to only change specific properties on an index’s object.

import * as algoliasearch from 'algoliasearch';
import * as functions from 'firebase-functions';

export const userOnUpdate = functions.firestore
   .document('users/{id}')
   .onCreate(async (change, context) => {
       const user = change.data();
       const client = algoliasearch('appId', 'apiKey');
       const index = client.initIndex('dev_users');
       return index.partialUpdateObject({
           objectID: change.id,
           ...user
       });
   });
Enter fullscreen mode Exit fullscreen mode

user.onDelete.ts

The userOnDelete trigger is the simplest operation with an initialize and delete object call to remove the Algolia object by the objectID we defined earlier.

import * as algoliasearch from 'algoliasearch';
import * as functions from 'firebase-functions';

export const userOnDelete = functions.firestore
   .document('users/{id}')
   .onCreate(async (change, context) => {
       const client = algoliasearch('appId', 'apiKey');
       const index = client.initIndex('dev_users');
       return index.deleteObject(change.id);
   });
Enter fullscreen mode Exit fullscreen mode

Export all of these constants to your root index.ts file. This will register them as new Firebase Cloud Functions when you build and deploy. At this point any time you change documents in Firestore (either directly through the Firebase Console or with your app) it will trigger these functions to push and sync data across to Algolia.

firebase deploy --only functions:userOnCreate,functions:userOnUpdate,functions:userOnDelete
Enter fullscreen mode Exit fullscreen mode

Application Side

You can store Algolia’s search-only access token (this is different than the apiKey used in Cloud Functions) in your environments file to easily access/import it.

Create a simple service to easily interact with your Algolia indexes.

user.service.ts

import * as algoliasearch from 'algoliasearch';

@Injectable()
export class UserService {

   client: algoliasearch.Client;

   init(config: {
       appId: string,
       apiKey: string
   }) {
       this.client = algoliasearch('appId', 'apiKey');
   }

   fetchUsers(options: algoliasearch.QueryParameters) {
       const userSearch = this.client.initIndex('dev_users');
       return userSearch.search(options);
   }

   fetchUsersByNameAsc(options: algoliasearch.QueryParameters) {
       const userSearch = this.client.initIndex('dev_users_name_asc');
       return userSearch.search(options);
   }

   fetchUsersByNameDesc(options: algoliasearch.QueryParameters) {
       const userSearch = this.client.initIndex('dev_users_name_desc');
       return userSearch.search(options);
   }

}
Enter fullscreen mode Exit fullscreen mode

In your component, provide UserService and make the following method calls to test the response back from Algolia.

async ngOnInit() {
    this.init({ appId: 'foo', apiKey: 'bar' });
    const res = await this.fetchUsers({
        page: 0,
        length: 10,
        query: 'Sean'
    });
    console.log('res', res);
}
Enter fullscreen mode Exit fullscreen mode

This method call will attempt to load the first page of results, up to 10 records that has a searchable attribute that matches “Sean”.

Final Thoughts

Without getting too far into the weeds of Algolia’s client and explicitly focusing on syncing data over and quickly logging that information out; we can see that Algolia serves as a powerful interface to receive the exact data we need.

In our implementation on Hive, we use Algolia to handle paginated admin tables, infinite scroll experiences, pre-filtering collection records by specific conditions and sorting table data. You can also leverage Algolia as a read-only database, only storing/syncing documents that the client should have access to. This is powerful when using concepts such as soft deletes, where you stamp a document with a deletedAt timestamp in Firestore and remove the object from Algolia. By doing this, you can always recover the document back, but all querying logic from Algolia will treat the document as being deleted.

Additional Resources

Top comments (0)