Firebase Firestore Text Search and Pagination

Implementing text search using array-contains and lazy loading.

Ken Tan
6 min readFeb 26, 2019
If only Firebase Firestore had a LIKE Operator like SQL.

As of version 5.8.0 (javascript), Firestore doesn’t have support for full text search. As mentioned on their documentation, we could subscribe to third party providers which is Agolia or Elastic Search for full text search. That’s great however for some of us who don’t want to use those services as they’re expensive, don’t want to maintain another service just for that functionality or for some other reason. This story will demonstrate on how you could leverage the array-contains query to filter records and adding lazy loading for paging.

I assume that you are already familiar with how to set up a Firebase project else please refer to their documentation.

Creating the Application

Lets create a simple application which has a records of people and we should be able to filter out the person that we’re looking for. This type of applications applies to patient records, payroll, and etc wherein our users are looking for someone by searching their name.

Will start off by putting a text box for searching and a table for displaying the records. I already have attach an event listener on document loaded for our scripts to be added later on.

Mark up for text box and table.
View on the browser.

Generating Records

Lets generate 200 random names using tools like Mackaroo. It should have properties: name (object) and keywords (array). The keywords field will be populated with strings of all the possibilities of a name is being search. Take note that array-contains is case sensitive, it would be much better that our names are formatted in lowercase. Lets create a function that would generate those keywords.

A function that takes name as an array then returns an array of keywords.

Calling the generateKeywords([‘john’, ‘the’, ‘dough’, ‘jr’]) would produce the output of:

Output of generateKeywords([‘john’, ‘the’, ‘dough’, ‘jr’])

That’s a lot of strings. We pass this array into the keywords property. In real world applications this is done on when creating or updating a person’s record.

The empty string is included so that when we query on the people collection with the keywords field, we should expect to get all the records instead of nothing.

Going back to our 200 random JSON names. We’ll iterate to each one of it and add the keywords property with a value getting from the generateKeywords function. Then add the document in people collection onto Firestore.

For screen shot purposes, 200 documents wouldn’t fit in so I just use one.

After running that script, our collection should look something like this:

People collection.

Implementing Search

Inside DOMContentLoaded event handler, lets create a function that takes a search as an argument which retrieves those documents from people collection and returns a markup to be pass on later to the tbody.

Retrieves documents from people collection where keywords contains the search then ordered by last name.

Invoke the searchByName with a empty string to display all people.

Updates the tbody html from the output of searchByName.

Run your application and you should see an error in the console saying:

Click on that link to build the index, wait for it to finish and then run or refresh your application again. You should see something like this:

Displays all the people in the table.

This is where the purpose of the empty string we added on the generateKeywords function because by default our app should display all the people when passed by an empty string on searchByName function.

As we type in the search box, the table is not being filtered. We must attach a keyup event handler on the text box.

Attach a keyup event on text box.

Try searching by last name, first name or first name first.

Typing on the search box filters out the documents.

Now we have implemented text search through array-contains. Our job here is done if we are happy of returning all the records to your users.

What if we have thousands of records? 🤔
And we are on Spark Plan which only has 50,000 document reads a day. 😅
Then hundreds of users are accessing this page. 😨

Even worse, if we’re on Blaze Plan with thousands of records all being accessed by hundreds of users. Billing Spike 😱💸💸💸😭

We don’t want to end up like this.

Limit to the rescue

We’ll be using .limit(). Let’s update our searchByName function.

Change parameter from string to an object with default values.

We changed the parameter from string to object which has search, and limit properties with default values.

We also have to change how we call our function.

String argument was omitted because a default value (empty string) is already provided.
Change argument from string to an object with search property and value of element that invoked callback function which is the text box.

Refresh your application and you should only see the first 50 people. As you type in the search box it filters out and still only displays first 50 people.

But what about those other people after the first 50. 🤔

Bring in Lazy Loading

We will implement a feature infinite scroll except that ours has finite number of records. It should have been called finite scroll. 😅

First we should be able to know when the vertical scroll bar is at the bottom.

Snippet taken from the internet to check whether the vertical scroll bar is at the bottom.

When the vertical scroll bar reaches the bottom of the page, we must get the last name of the last person. Replace the “do something” comment with:

Gets the last name of the last row of table body.

We need the last name of last person for query cursor which is how Firestore implements pagination. This gives as the ability set a starting point for looking up documents. Lets use the startAfter method. In our query .orderBy(‘name.last’) chain another method .startAfter(lastNameOfLastPerson)

Chain .startAfter(lastNameOfLastPerson) after .orderBy(‘name.last’)

And also add another property lastNameOfLastPerson on the parameter.

Add lastNameOfLastPerson property with a default value of empty string

Now the searchByName functions will return records depending on the last name of the last person.

Lets pass this last name of last person into searchByName function and then update the existing table rows. The infinite scroll syntax should look like this:

Implementation of infinite scrolling.

Lets try it out. 😁

When vertical scroll bar reaches the bottom of the screen, it loads up the next set of records, and so on.

That is it. This application is hosted here.
Link for the source code.
If you have allergies with .innerHTML syntax, I’ve create another branch without using it.

Thank you for reading my first story. Happy to hear your reviews. 😇

--

--