Bindings

An index binding is a Lambda function that computes the value for a field in a document while the document is being indexed. Once defined, the binding can be applied to the index’s terms field, which allows you to search for computed values, or to the index’s value field, which allows you to return computed values for index matches.

For example, the documents that we are indexing might include a timestamp. We might want to be able to search for documents by date, or we might want to return the day of week for index matches.

Bindings must be pure Lambda functions: they must not create side effects, such as any additional reads or writes. They are provided with a copy of the document to be indexed and must operate on the document’s values.

Functions that cannot be used in bindings include:

This tutorial assumes that you have successfully prepared your database by creating the necessary collections and documents.

Create and use a binding

An index’s source field defines one or more collections that should be indexed. It is also used to define zero or more fields that have associated binding functions. The binding functions compute the value for the specified field while the document is being indexed.

Once a binding is defined, it must be used at least once in either the index’s terms or values fields, or an error occurs: a binding definition without use would cause unnecessary computation.

Let’s create an index for the People collection (that we created previously) that specifies a binding function. Our binding function calculates the "rolodex letter" for each person, which is the first letter of their last name.

CreateIndex({
  name: "people_by_rolodex",
  source: {
    collection: Collection("People"),
    fields: {
      rolodex: Query(
        Lambda(
          "doc",
          SubString(Select(["data", "last"], Var("doc")), 0, 1)
        )
      )
    }
  },
  terms: [ { binding: "rolodex" }],
  values: [
    { binding: "rolodex" },
    { field: ["data", "last"] },
    { field: ["ref"] }
  ]
})

The highlights of this query:

  • The index is called people_by_rolodex.

  • The source field specifies that:

    • Documents within the People collection should be indexed.

    • The field rolodex (which does not have to exist in any document in the collection) has a Lambda function,

    • The Query function defers execution of the Lambda function until a document needs to be indexed.

    • The Lambda function itself accepts the current document as a variable called doc. Then it returns the first letter of the document’s last field using the SubString function. The Select function is used to extract the last field from the document.

  • The terms field specifies that the rolodex binding’s result is to be used for searching this index.

  • The values field specifies that the rolodex binding’s result is to be included for index matches, along with the document’s Reference.

When you run this query, the output should be similar to:

{ ref: Index("people_by_rolodex"),
  ts: 1580948872710000,
  active: true,
  serialized: true,
  name: 'people_by_rolodex',
  source:
   { collection: Collection("People"),
     fields:
      { rolodex:
         Query(Lambda("doc", Substring(Select(["data", "last"], Var("doc")), 0, 1))) } },
  terms: [ { binding: 'rolodex' } ],
  values:
   [ { binding: 'rolodex' },
     { field: [ 'data', 'last' ] },
     { field: [ 'ref' ] } ],
  partitions: 1 }

Now we can find people by their "rolodex letter". Copy the following query, paste it into the Shell, and run it:

Paginate(Match(Index("people_by_rolodex"), "C"))

The output should be similar to:

{
  data: [
    ["C", "Cook", Ref(Collection("People"), "239535822978682381")],
    ["C", "Cook", Ref(Collection("People"), "239535822978684429")]
  ]
}

Search for empty fields

One specific use of bindings that you might find to be very useful is to locate empty fields. Fauna does not store empty fields, and you cannot directly use null with the Match function.

For example, with the following index:

CreateIndex({
  name: "people_by_letter",
  source: Collection("People"),
  terms: [ { field: ["data", "letter"] } ]
})

We can search for people by letter:

Paginate(Match(Index("people_by_letter"), "A"))
{ data: [ Ref(Collection("People"), "240166254282805769") ] }

However, we cannot search for empty values using this index:

Paginate(Match(Index("people_by_letter"), null))
{ data: [] }

Why does this happen? When we pass null to the Match function, the query compares the indexed field values to null. A field that is not set has no value at all, whereas null is a value: the two don’t match. Alternately, if we do not pass a value to Match, there is an empty comparison value to compare with the index’s terms fields. Since none of the indexed entries is lacking a terms field, none of the indexed entries matches the empty comparison value.

Instead, we can create a binding that tells us when a field is unset:

CreateIndex({
  name: "people_by_null_letter",
  source: [{
    collection: Collection("People"),
    fields: {
      null_letter: Query(
        Lambda(
          "doc",
          Equals(Select(["data", "letter"], Var("doc"), null), null)
        )
      )
    }
  }],
  terms: [ {binding: "null_letter"} ],
})

The highlights for this query:

  • The index is named people_by_null_letter.

  • The source is the People collection.

  • A binding for the field null_letter is defined.

  • The binding function:

    • Accepts the indexed document in the doc variable.

    • Uses the Select function to pull out the letter field, and to use null as the default value is the field is unset.

    • Uses the Equals function to compare the value of the letter field with null. The result of calling this function is the implicit return value for the binding function, and that value is either true or false.

  • The terms field specifies the binding function’s return value as a search term.

With this index in place, we can now search for People documents that have unset letter fields:

Map(
  Paginate(Match(Index("people_by_null_letter"), true)),
  Lambda("X", Get(Var("X")))
)
{
  data: [
    {
      ref: Ref(Collection("People"), "240166254282803721"),
      ts: 1580867425600000,
      data: {
        first: "Leslie",
        last: "Lamport",
        degrees: ["BS", "MA", "PhD"]
      }
    }
  ]
}

Conclusion

This tutorial has demonstrated how to define and use index bindings, which helps us achieve specific kinds of search results.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!