How to Get a Full-Text Search with FaunaDB

Last Update: 12.10.2020. By in Javascript

I’ve been using FaunaDB for a few projects now, as it is the only direct database integration for Netlify functions.

Last time I needed a simple wildcard search. In SQL, this is a simple where FIELD like "%term%". Easily combined with any other filtering or ordering.

Turned out that it is not straightforward in FaunaDB.

The usual filtering is done via indexes and a MATCH query. Unfortunately, MATCH only supports EXACT matches, no wildcards. And sadly, no further explanation at that place in the docs on how to achieve this.

Reading the docs, googling, and stackoverflowing finally brought me to the “About full-text search” section in this getting started guide.

The strategy is to put a Filter around your index. An index in Fauna is more like a search query in SQL and not an SQL index.

When you define a simple index like in their example:

CreateIndex({
  name: "all_Planets",
  source: Collection("Planets")
})

it’s merely a select * from Planets in SQL.

Based on that, the proposed way to get a full-text search in the article was that:

Map(
  Filter(
    Paginate(Match(Index("all_Planets"))),
    Lambda("planetRef",
      ContainsStr(
        LowerCase(Select(["data","name"],Get(Var("planetRef")))),
        "ur"
      )
    )
  ),
  Lambda("planetRef", Get(Var("planetRef")))
)

Let’s break that down.

  1. We get all records from the collection Index and Match
  2. We put pagination around that with Paginate
  3. The Result of that is filtered with Filter and giving that a single Lambda
  4. This Lambda a. Get the item ref as a parameter “planetRef” b. Retrieves the value behind it (the actual ref object with Var) c. Get the document object for that Get d. Gets the field value “data.name” of that document Select e. Makes that value to lowercase LowerCase f. Checks if this value contains our search term “ur” with ContainsStr
  5. And finally maps all result item refs again to the actual document Map and the Lambda with Get

If you are coming from SQL like me, this looks like many queries hitting the DB. And as far as I understood it from various questions, they all count as a “reads” in your account.

Now, my use case was a bit different.

My index already contained a search term and also defined the fields returning. That changes the return type of the index to an array– one entry for each field.

A simplified version looks like:

CreateIndex(
{
  name: "all_feedforms_by_user_desc",
  unique: false,
  serialized: true,
  source: Collection("feedforms"),
  terms: [
    {
      field: ["data", "user_id"]
    }
  ],
  values: [
    {
      field: ["data", "creation_date"], reverse: true
    },
    {
      field: ["data", "user_id"]
    },
    {
      field: ["data", "formName"]
    },
    {
      field: ["ref"]
    },
}

I only want those records which match a given userId and some fields that I show in an overview in my app. Additionally, I sort the records by descending creation time.

Using my index and adapting the code above resulted in errors because my variable for the inner Lambda was an array. And I found absolutely no way to access a single value in it.

The Lambda does accept only a single parameter. However, this parameter can also be an array of parameter names. Then it will map the resulting array from the index with the array of parameter names you define for the Lambda. Thus, you can access each array index as a parameter.

Now I could access the single field value I am interested in and remove all those Get document loads.

Filter(
    Paginate(Match(Index('all_feedforms_by_user_desc'), "myuserid" )),
        Lambda(
          ['date_created', 'user', "formName", "refid"],
          ContainsStr(
            LowerCase(Var("formName")),
            searchTerm
          )
    )
)
  1. I get all records for a given userId from the Index with Match
  2. Paginate that and then filter it
  3. The filter Lambda gets an array of parameter names matching exactly the return array of the index.
  4. Checking if the value behind Lambda parameter “formName” contains the “searchTerm”

The result, a simple wildcard search without the need to get the document multiple times.