by Brecht De Rooms

5 common problems FaunaDB indexes solve for you

how-to
Nov 20, 201910 mins
DatabasesNoSQL DatabasesSoftware Development

You can use indexes not only to speed up queries, but also to paginate, sort, and filter documents in FaunaDB

faunadb hero
Credit: Fauna

Indexes are a core feature of almost every database today, so it’s no surprise that FaunaDB has them too. FaunaDB is a cloud-hosted, distributed, multi-model database that is well-suited for web and mobile apps.

FaunaDB offers native support for GraphQL, but also provides its own, more powerful query language, FQL, which we’ll use in our indexing examples here. 

There are multiple use cases for indexes. The most obvious is improved performance when locating specific data within vast collections of records, but pagination, sorting, and searching are also provided by indexes.

Every time we want to satisfy some data requirements that target one or more documents, and we don’t know their refs (the global reference that identifies a unique document within a database), indexes are the way to go.

In this article, we will learn about five common use cases that can be solved with indexes. Every use case has a code example and an explanation.

  1. Sort documents
  2. Return specific fields from an index (to avoid fetching the complete document)
  3. Search documents
  4. Filter one collection with results of another
  5. Force field combinations to be unique

So let’s get started! Log in to https://dashboard.fauna.com/, create a new database, and then use the Shell.

Create a collection

An index works on a collection, so we need to set up some collections in FaunaDB first before we dive into our examples.

Here is the FQL for this:

CreateCollection({ name: "customers" });
Map(
  [
    { name: "Ali", purchase: "pan", otherInfo: "other data" },
    { name: "Tina", purchase: "bowl", otherInfo: "other data" },
    { name: "Frank", purchase: "bowl", otherInfo: "other data" }
  ],
  Lambda("customer",
    Create(Collection("customers"), { data: Var("customer") })
  )
);
CreateCollection({ name: "products" });
Map(
  [
    { name: "pan", price: 500 },
    { name: "bowl", price: 100 },
    { name: "cup", price: 50 }
  ],
  Lambda("product",
    Create(Collection("products"), { data: Var("product") })
  )
);

We use a customers collection that holds three documents, which in turn hold three fields; name, purchase, and otherInfo.

We also use a products collection that holds a product’s name and price.

Use an index to sort documents 

The first and most straightforward use case for indexes is sorting. Even the collection indexes, which are automatically created when we create a collection in the Console, are sorted by their refs.

faunadb indexes 01 Fauna

Most of the time, we want to retrieve our data sorted in some order. For example, we have customers with a name, and we want to sort them alphabetically.

An index can be created with a set of values; these are fields within the document data that are used for sorting, and the index is updated whenever we create or update a document. We have to create an index for every different way that we want to sort our documents, like name ascending, by name descending, by purchase ascending, or by purchase descending.

The beautiful thing with FaunaDB indexes is, indexes can be created at any time. That means we don’t have to imagine all of the possible ways that we want to sort our data right at the start.

Let’s look at this FQL code example to create an index:

CreateIndex({
  name: "customers_by_name_asc",
  source: Collection("customers"),
  values: [
    { field: ["data", "name"] },
    { field: ["ref"] }
  ]
});

We can then retrieve the documents in a sorted fashion with: 

Map(
  Paginate(Match(Index("customers_by_name_asc"))),
  Lambda(["name", "ref"], Get(Var("ref")))
);

First, we create an index for our collection. It holds two values, the name that we sort by, and the ref that we use to retrieve the actual document from the customers collection later.

faunadb indexes 02 Fauna

Second, we pass pages from our index into the Map function. 

faunadb indexes 03 Fauna

Because our index specifies two fields for values, every entry in our index contains a values array; its first item is the name of our customer; the second item is the ref.

We use the ref and pass it to the Get function. Because a ref globally identifies a document in our database, Get returns the document that we want to fetch.

faunadb indexes 04 Fauna

Here we use the Map function to loop over references of customer documents and use Get to retrieve the actual document. Since the index was sorted by its values, first by name then by ref, the documents will be retrieved in that same order.

Use an index to return specific fields 

The goal here is to slim down the response, because the fastest byte is a byte not sent! By returning specific fields from an index, we can avoid fetching the complete document. 

We achieve this by creating an index that holds the values that we are interested in.

Usually, when we want to fetch some documents, we would first consult an index to retrieve the refs, and then use these refs to fetch the documents (with Map and Get) from the collection. 

Because Fauna indexes are like views, we can create a separate index that holds all of the field values of interest as values, so that we can use the values directly without having to fetch the documents themselves.

CreateIndex({
  name: "customers_name_purchase",
  source: Collection("customers2"),
  values: [
    { field: ["data", "name"] },
    { field: ["data", "purchase"] },
    { field: ["ref"] }
  ]
});

The name of the index, customers_name_purchase, tells us that it’s an index of the customers collection, where the first value is the name and the second value is the purchase. To retrieve the data we need, we can run the following query:

Paginate(Match(Index("customers_name_purchase")));
faunadb indexes 05 Fauna

In the previous example, we fetched documents from the index and then needed to retrieve the document from the collection using Get. This time we have created an index that has three values: the name, purchase, and ref of a customer document. When we have a use case that only needs these values, we can now simply fetch pages. Since the index already contains the values we need, we do not need to retrieve the full document anymore. The result would look something like this:

{
  data: [
    ['Ali', 'pan', Ref(Collection("customers"), "246578504281358850")],
    ['Frank', 'bowl', Ref(Collection("customers"), "246578504281360898")],
    ['Tina', 'bowl', Ref(Collection("customers"), "246578504281359874")]
  ]
}

Use an index to search documents 

The next use case is searching for specific documents, or filtering documents. For this, we have to create an index that contains the fields we want to make searchable as terms.

In this example, the index will be created with terms instead of values.

CreateIndex({
  name: "customers_search_by_name",
  source: Collection("customers"),
  terms: [
    { field: ["data", "name"] }
  ]
});

Retrieving the document is similar to the first example but this time we provide an attribute to match on (“Tina” in this case).

Map(
  Paginate(Match(Index("customers_search_by_name"), "Tina")),
  Lambda("ref", Get(Var("ref")))
);

When creating an index, the terms field is used for searching or filtering, and the values field is used for sorting or providing results.

faunadb indexes 06 Fauna

Also, values are retrieved when reading the index with Paginate(Match(Index(…))) but terms are not.

Since we did not provide this index with values, the index only contains refs. The result of Paginate will, therefore, be an array of refs and not an array of values. This time, our Lambda has only one argument instead of multiple arguments that directly pass into the Get function to retrieve the document.

As in the previous example, if we need to load the purchase of a specific customer, we could add the purchase field to the values of the index, and we wouldn’t have to call Get on the collection anymore.

Use an index to filter one collection with results of another

Let’s look at a more complex example. We have two collections. One that holds all of our products and one that holds all of our customers and their last purchase. Now we want to retrieve only the products that are the last purchases of our customers.

Here is the FQL to build the needed indexes:

CreateIndex({
  name: "customer_purchases",
  source: Collection("customers"),
  values: [
    { field: ["data", "purchase"] }
  ]
});
CreateIndex({
  name: "product_names",
  source: Collection("products"),
  terms: [
    { field: ["data", "name"] }
  ]
});

And the code to retrieve the products, based on the last customer purchases:

Map(
  Paginate(Distinct(Match(Index("customer_purchases")))),
  Lambda(
    "product_name",
    Get(Match(Index("product_names"), Var("product_name")))
  )
);

We have created two indexes. The first is an index with values, as we have seen in the previous example indexes with values sorted. In this case, the first index is sorted on the purchase field from the customers collection. The second index is an index based on the products collection. Since the first index is sorted on purchase and results are paginated, we can use that index to get a page with the last customer purchases and then use the second index to retrieve the products.

The Distinct function filters out all of the duplicates, so we end up with an array of product names, but every product name only appears once in the array that is returned.

With the Map function, we pass the name of every product into the Match function and use its return value, a ref, to retrieve the product documents from their collection.

Use an index to force field combinations to be unique

The example in this section is about keeping index entries, and their associated documents, unique. When creating an index, we can use the unique parameter, which only allows one entry with a specific values/terms combination.

The following code will create a collection and an index that requires the combination of name, state, and country to be unique. 

CreateCollection({ name: "cities" });
CreateIndex({
  name: "cities_unique",
  source: Collection("cities"),
  unique: true,
  values: [
    { field: ["data", "name"] },
    { field: ["data", "state"] },
    { field: ["data", "country"] }
  ]
});

The index that we created uses the unique parameter, so all of the values that we added to it have to be a unique combination when we create a new document in the cities collection.

The following Create call returns a new document with its ref:

Create(Collection("cities"), {
  data: { name: "Mexico City", state: "Mexico City", country: "Mexico" }
});

The next Create call uses a new combination of name, state, and country.

Create(Collection("cities"), {
  data: { name: "New York City", state: "New York", country: "USA" }
});

Note that if we try to add a combination that already exists, the Create call will fail with the error message “instance not unique.”

Create(Collection("cities"), {
  data: { name: "New York City", state: "New York", country: "USA" }
});

Live better with indexes

Indexes are the way to get things done with FaunaDB. They are not just some speed improvements that we drop in when queries aren’t performing as expected; they are the backbone of the whole enterprise! Indexes allow us to paginate, sort, and filter documents and even to expose constraints on their composition. They are needed every time we want to fetch multiple documents and don’t know their refs.

Brecht De Rooms is senior developer advocate at Fauna. He is a programmer who has worked extensively in IT as a full-stack developer and researcher in both the startup and IT consultancy worlds. It is his mission to shed light on emerging and powerful technologies that make it easier for developers to build apps and services that will captivate users.

New Tech Forum provides a venue to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to newtechforum@infoworld.com.