Indexes

Reference: Index definitions

An index stores, or covers, specific document field values for quick retrieval. You can use indexes to filter and sort a collection’s documents in a performant way.

Using indexes can significantly improve query performance and reduce costs, especially for large datasets. Unindexed queries should be avoided.

Define an index

You create and manage indexes at the collection level as part of an FSL collection schema. An index definition can include:

  • Terms: Document fields for exact match searches

  • Values: Document fields for sorting and range searches

You can only index persistable field values.

An index definition must include at least one term or value. A collection can include multiple index definitions:

collection Product {
  ...

  // Defines the `byName()` index.
  index byName {
    // `terms` are document fields for exact match searches.
    // In this example, you get `Product` collection documents
    // by their `name` field value.
    terms [.name]
    // `values` are document fields for sorting and range searches.
    // In this example, you sort or filter index results by their
    // descending `quantity` field value.
    values [desc(.quantity)]
  }

  ...
}

You manage and submit schemas to Fauna using the Fauna Dashboard or Fauna CLI.

Index builds

When you submit a collection schema with a new or updated index definition, Fauna starts an index build.

During the build, the index stores, or covers, these fields for each document in the collection. During a build, the index may not be queryable.

If the collection contains more than 128 documents, Fauna uses a background task to build the index. The limit prevents an excessive number of concurrent builds.

System collection

Fauna stores schemas for user-defined collections, including their index definitions, as documents in the Collection system collection. These documents act as an FQL version of the FSL collection schema. You can use Collection methods to access index definitions in FQL.

Call an index

In an FQL query, you call an index as a method on its collection:

// Call the `byName()` index to fet `Product` collection
// documents with a `name` value of `limes`. Values must
// match exactly.
Product.byName("limes")

The call returns a set of matching collection documents.

Reference: FQL index method docs

Terms

You can use index terms to run exact match searches on document field values.

The following index definition includes name as an index term:

collection Product {
  ...

  index byName {
    terms [.name]
  }

  ...
}

When you call the index, you must pass an argument for each term in the index definition.

// Get products named "limes"
Product.byName("limes")

The call returns a set of Product collection documents with a name of limes.

Pass multiple index terms

The following index definition includes two index terms:

collection Customer {
  ...

  index byName {
    terms [.firstName, .lastName]
  }
}

In an index call, use a comma to separate term arguments. Provide arguments in the same field order used in the index definition.

// Get customers named "Alice Appleseed"
Customer.byName("Alice", "Appleseed")

The call returns a set of matching collection documents.

Missing or null terms

If an index definition contains terms, Fauna doesn’t index a document if all its index terms are missing or otherwise evaluate to null. This applies even if the document contains index values.

Best practices for index terms

Avoid using frequently updated fields as index terms.

Internally, Fauna partitions indexes based on its terms, if present. Frequent updates to term field values trigger updates to these partitions.

If you need to filter or run an exact match search on a frequently updated field, consider adding the field as an index value instead:

collection Product {
  ...
  // Defines the `sortedByCategory()` index.
  // The index includes the `category` field as an index value.
  // `category` is a frequently updated field.
  index sortedByCategory {
    values [.category, .name, .description, .price]
  }
}

Then use the index to run a range search on the index value:

// Uses the `sortedByCategory()` index to run a range search
// on `category` field values. The query only retrieves `Product`
// collection documents with a `category` of `sports`. The query
// is covered and avoids document reads.
Product.sortedByCategory({ from: "sports", to: "sports" }) {
  name,
  description,
  price
}

Values

You can use index values to sort a collection’s documents. You can also use index values for range searches.

Sort collection documents

The following index definition includes several index values:

collection Product {
  ...

  index sortedByPriceLowToHigh {
    values [.price, .name, .description]
  }
}

Call the sortedByPriceLowToHigh() index with no arguments to return Product documents sorted by:

  • Ascending price, then …​

  • Ascending name, then …​

  • Ascending description, then …​

  • Ascending id (default)

// Get products by ascending price, name, and description
Product.sortedByPriceLowToHigh()

Descending order

By default, index values sort results in ascending order. To use descending order, use desc() in the index definition:

collection Product {
  ...

  index sortedByPriceHighToLow {
    values [desc(.price), .name, .description]
  }

  ...
}

Call the index with no arguments to return Product documents sorted by:

  • Descending price, then …​

  • Ascending name, then …​

  • Ascending description, then …​

  • Ascending id (default)

// Get products by descending price,
// ascending name, and ascending description
Product.sortedByPriceHighToLow()

You can also use index values for range searches.

The following index definition includes several index values:

collection Product {
  ...

  index sortedByPriceLowToHigh {
    values [.price, .name, .description]
  }
}

The index specifies price as its first value. The following query passes an argument to run a range search on price:

// Get products with a price between
// 20 (inclusive) and 30 (inclusive)
Product.sortedByPriceLowToHigh({ from: 20, to: 30 })

If an index value uses descending order, pass the higher value in from:

// Get products with a price between
// 20 (inclusive)and 30 (inclusive) in desc order
Product.sortedByPriceHighToLow({ from: 30, to: 20 })

Omit from or to to run unbounded range searches:

// Get products with a price greater than or equal to 20
Product.sortedByPriceLowToHigh({ from: 20 })

// Get products with a price less than or equal to 30
// Product.sortedByPriceLowToHigh({ to: 30 })

Pass multiple index values

Use an array to pass multiple value arguments. Pass the arguments in the same field order used in the index definition.

Product.sortedByPriceLowToHigh({ from: [ 20, "l" ], to: [ 30, "z" ] })

The index returns any document that matches the first value in the from and to arrays. If matching documents have the same values, they are compared against the next array element value, and so on.

For example, the Product collection’s sortedByPriceLowToHigh() index covers the price and name fields as index values. The Product collection contains two documents:

Document

price

name

Doc1

4.99

pizza

Doc2

6.98

cups

The following query returns both Doc1 and Doc2, in addition to other matching documents:

Product.sortedByPriceLowToHigh({ from: [4.99, "p"] })

The first value (4.99 and 6.98) of each document matches the first value (4.99) of the from array.

Later, you update the document values to:

Document price name

Doc1

4.99

pizza

Doc2

4.99

cups

The following query no longer returns Doc2:

Product.sortedByPriceLowToHigh({ from: [4.99, "p"] })

Although the first value (4.99) in both documents matches the first value in the from array, the second value (cups) in Doc2 doesn’t match the second value (p) of the from array.

Run a range query on id

All indexes implicitly include an ascending document id as the index’s last value.

If you intend to run range queries on id, we recommend you explicitly include an ascending id as the last index value in the index definition, even if you have an otherwise identical index.

For example, the following sortByQuantity() and sortByQuantityandId() indexes have the same values:

collection Product {
  ...

  index sortByQuantity {
    values [.quantity]
  }

  index sortByQuantityandId {
    values [.quantity, .id]
  }

  ...
}

Although it’s not explicitly listed, sortByQuantity() implicitly includes an ascending id as its last value.

To reduce your costs, Fauna only builds the sortByQuantity() index. When a query calls the sortByQuantityandId() index, Fauna uses the sortByQuantity() index behind the scenes. sortByQuantityandId() only acts as a virtual index and isn’t materialized.

Missing or null values

If an index definition contains only values, Fauna indexes all documents in the collection, regardless of whether the document’s index values are missing or otherwise null.

When sorting, Fauna treats null index values as the last value in ascending order and as the first value in descending order.

Combine terms and values

If an index has both terms and values, you can run an exact match search on documents in a provided range.

// Get products named "pinata"
// with a quantity between 10 (inclusive) and 50 (inclusive)
Product.byName("pinata", { from: 50, to: 10 })

Index an array field

By default, Fauna assumes index term and value field contain scalar values. Use mva() to index an Array field’s values:

collection Product {
  ...
  index byCategory {
    // `categories` is an array field.
    terms [mva(.categories)]
  }

  index sortedByCategory {
    // `categories` is an array field.
    values [mva(.categories)]
    // You can combine `mva()` with
    // `desc()` and `asc()`. Ex:
    // values [desc(mva(.categories))]
  }
  ...
}

mva() only works on the last item in the provided field accessor. For more complex nested arrays, such as an object array, use a computed field:

collection Order {
  ...
  // `Order` collection document
  // have the following structure:
  // {
  //   customer: Customer("<CUSTOMER_DOC_ID>"),
  //   cart: [
  //     {
  //       product: Product("PRODUCT_DOC_ID"),
  //       quantity: 10,
  //       price: 6.98
  //     },
  //     ...
  //   ],
  //   ...
  // }

  // Defines the `prices` computed field.
  // The field uses `map()` to extract `price` values
  // from the `cart` array's object to a flat array.
  compute prices = (.cart.map(cartItem => cartItem.price))

    // Uses `mva()` to index the computed `prices` field.
  index byPrices {
    terms [mva(.prices)]
  }
  ...
}

Index document relationships

You can use an index to query documents by their relationships.

collection Product {
  ...
  // The `store` field accepts `Store` collection documents.
  store: Ref<Store>
  ...

  // Defines the `byStore()` index.
  // Use the index to get `Product` collection
  // documents by `store` value. In this case,
  // `store` contains `Store` collection documents.
  index byStore {
    terms [.store]
  }
}

For examples of using an index to query document relationships, see Query by document relationship.

Don’t index document IDs

When indexing a field that contains document, index the entire field, not the document ID:

collection Product {
  ...
  // The `store` field accepts `Store` collection documents.
  store: Ref<Store>
  ...

  // Correct:
  index byStore {
    terms [.store]
  }

  // Incorrect:
  // Indexing `store.id` fails.
  // Instead, index by the entire `store`field.
  index byStoreId {
    terms [.store.id]
  }
}

IDs aren’t persistable and can’t be indexed.

Covered queries

If you project an index’s covered term or value fields, Fauna gets the field values from the index. This is a covered query.

// This is a covered query.
// `name`, `description`, and `prices` are values
// in the `sortedByPriceLowToHigh()` index definition.
Product.sortedByPriceLowToHigh() {
  name,
  description,
  price
}

If the projection contains an uncovered field, Fauna must retrieve the field values from the documents. This is an uncovered query.

// This is an uncovered query.
// `quantity` is not one of the terms or values
// in the `sortedByPriceLowToHigh()` index definition.
Product.sortedByPriceLowToHigh() {
  quantity,
  name
}

Covered queries are typically faster and less expensive than uncovered queries, which require document reads. If you frequently run uncovered queries, consider adding the uncovered fields to the index definition.

Partitions

When an index has one or more terms, the index is internally partitioned by its terms. Partitioning lets Fauna scale indexes efficiently.

Virtual indexes

To reduce your costs, Fauna doesn’t build duplicate indexes that have the same terms and values. Instead, Fauna only builds a single index and internally points any duplicates to the single index.

For example, in the following collection, the byDescription() and byDesc() indexes are duplicates:

collection Product {
  ...

  index byDescription {
    terms [.description]
  }

  index byDesc {
    terms [.description]
  }
}

When a query calls the byDesc() index, Fauna uses the existing byDescription() index internally. byDesc() is considered a virtual index and is never materialized.

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!