Full Text Search with Firestore on Android

A common use case in Android applications is to provide the user with the functionality to search through a list of records. While Firestore is a solid database to power read heavy applications, it does not offer an out of the box, full text search.

The official Firestore documentation regarding full text search suggests us to use a third-party search service such as Elastic Search or Algolia. However, choosing the right third-party search service comes with its own overheads such as pricing, maintaining an additional service layer etc.

So, is there a simpler way to implement full text search without a third-party service?

Enter, Hackerman.

If you haven’t already setup Firestore with your project, you can follow the official documentation to get started.

The Use Case

Let’s say that we’re building an Android application that lists a set of products available at a grocery shop. To keep things simple, let’s assume that each product has just two fields — its name and its price.

Here’s how our schema looks like in Firestore, with a root level collection called products and each document inside it corresponds to a single grocery item.

Solution Overview

Our solution comprises of the following steps:

  1. Setting up our Firestore collection
  2. Generating keywords for each product name
  3. And finally, use whereArrayContains to power our search functionality

Before we continue, it’s highly important to understand the limitations of this approach.

  1. It is not possible to query a Firestore database with more than one whereArrayContains
  2. Firestore indices have limits, and as our dataset expands, it is possible that we’ll hit this upper limit

Understanding these limitations is crucial as these could become major blockers as your application scales.

Generating Keywords

The keywords array will power our search functionality. When we query Firestore in the next steps, we will check if our search query is contained in the keywords array for any document. Therefore, we want to ensure that our keywords array contains an exhaustive set of entries that cover all possible combinations of the product’s name. There are multiple ways to generate these keywords.

Following the first approach, here’s how it looks like when translated to Kotlin:

fun generateKeywords(name: String): List<String> {
val keywords = mutableListOf<String>()
for (i in 0 until name.length) {
for (j in (i+1)..name.length) {
keywords.add(name.slice(i until j))
}
}
return keywords
}

Here are some keywords generated for — Premium Filter Coffee 250gm

Now that we have our data ready, we can add it to Firestore. Each document now has three fields — name, price and keywords.

data class Product (
val name: String,
val price: Double,
val keywords: List<String>
)

and can be added to our database with the following piece of code:

Firebase
.firestore
.collection("products")
.add(Product(name, price, generateKeywords(name))

Querying Firestore

Let’s write a method that returns our database query:

fun getProductsByName(searchQuery: String) = Firebase.firestore
.collection("products")
.whereArrayContains("keywords", searchQuery.trim())
.limit(50)

Limits are optional, but recommended, as they throttle our search results and ensure that we don’t accidentally overuse our Firestore free limits / budgets.

We can now integrate this method directly with our UI or interface it with a view model. I prefer using the latter approach.

private val observer = MutableLiveData<List<Product>>()
fun liveData(): LiveData<List<Product>> = observer
fun getProductsByName(searchQuery: String) =
mRepository
.getProductsByName(searchQuery)
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful)
observer.postValue(task.result.toObjects(Product::class.java))
}

And that’s it!

That’s how we can power full text search in Firestore with its native, out of the box capabilities. However, I would like to reiterate its limitations so you can make the best decision for your application considering the use case and your roadmap.

I would also suggest for you to debounce your search listeners, to optimise the number of queries we end up making to the Firestore database.

Leave a Reply

Your email address will not be published. Required fields are marked *