Check out v4 of the Fauna CLI

v4 of the Fauna CLI is now in beta.

The new version introduces enhancements to the developer experience, including an improved authentication workflow. To get started, check out the CLI v4 quick start.

FSL collection schema: Index definitions

See Indexes

This page covers the FSL syntax for index definitions. For an overview of indexes, see Indexes.

An index stores, or covers, specific document field values for quick retrieval. Using indexes can significantly improve query performance and reduce costs, especially for large datasets.

You define indexes as part of an FSL collection schema.

collection Order {
  ...
  // Index definition.
  // You use indexes to filter and sort documents
  // in a performant way.
  index byCustomer {
    terms [.customer]
    values [desc(.createdAt), desc(mva(.statuses))]
  }
  ...
}

You can create and manage schema using any of the following:

When you submit a new or updated collection schema, Fauna may need to build (or rebuild) the collection’s indexes. See Index builds.

Fauna stores each collection schema as an FQL document in the Collection system collection. The Collection document’s indexes field contains FQL versions of the collection’s index definitions.

FSL syntax

index <indexName> {
  [terms [<term> . . .]]
  [values [<value> . . .]]
}

Name

indexName String Required

Index name. Must be unique within the collection.

Properties

Property Type Required Description

terms

Array of field accessors

Yes, if values is not defined.

Fields used for exact match searches.

Supports dot notation and bracket notation. You can only index persistable field values.

Use mva() to index an Array field’s values.

You can only index persistable field values.

values

Array of field accessors

Yes, if terms is not defined.

Fields used for sorting and range searches.

Supports dot notation and bracket notation. You can only index persistable field values.

Use mva() to index an Array field’s values.

Use asc() or desc() to sort results in respective ascending or descending order. Defaults to asc (ascending).

You can only index persistable field values.

Index document relationships

When you index a field that contains a document, you index a document reference. The reference consists of the document’s collection and document ID:

collection Product {
  ...
  // The `category` field contains a reference to
  // a `Category` collection document.
  category: Ref<Category>
  ...
  // Indexes the `category` field as an index term.
  // The index stores `Category` document references.
  // Example reference: Category("123")
  index byCategory {
    terms [.category]
  }
}

An index can’t store a referenced document’s fields. An index also can’t store a computed field that references another document. See Patterns to avoid.

Missing or null values

  • 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.

  • 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.

Examples

Basic example

You define an index in an FSL collection schema:

collection Product {
  ...
  index byName {
    terms [.name]
  }
  ...
}

Once the index is built, you call it as a method on the 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.

Use index terms for exact match searches

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.

Use index values for sorting and range searches

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

Sort 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()

Sort in 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_00 (inclusive) and 30_00 (inclusive)
Product.sortedByPriceLowToHigh({ from: 20_00, to: 30_00 })

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

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

Omit from or to to run unbounded range searches:

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

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

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_00, "l" ], to: [ 30_00, "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 search on id

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

If you intend to run range searches 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 sortByStock() and sortByStockandId() indexes have the same values:

collection Product {
  ...

  index sortByStock {
    values [.stock]
  }

  index sortByStockandId {
    values [.stock, .id]
  }

  ...
}

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

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

Pass terms and values

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

The following index definition includes name as an index term and stock as an index value:

collection Product {
  ...

  index byName {
    terms [.name]
    values [.stock]
  }

  ...
}

When you call the index, you must provide a term and can specify an optional range:

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

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 documents
  // have the following structure:
  // {
  //   customer: Customer("<CUSTOMER_DOC_ID>"),
  //   items: [
  //     {
  //       product: Product("PRODUCT_DOC_ID"),
  //       quantity: 10
  //     },
  //     ...
  //   ],
  //   ...
  // }

  // Defines the `quantities` computed field.
  // The field uses `map()` to extract `price` values
  // from the `cart` Array's object to a flat Array.
  compute quantities = (.items.map(item => item.quantity))

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

Fauna does not return the non_covered_document_read performance hint for uncovered index queries that pass an mva() term.

Covered queries

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

The following index definition includes several index values:

collection Product {
  ...

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

The following is a covered query:

// This is a covered query.
// `name`, `description`, and `price` 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.
// `stock` is not one of the terms or values
// in the `sortedByPriceLowToHigh()` index definition.
Product.sortedByPriceLowToHigh() {
  name,
  stock
}
Performance hint: non_covered_document_read

Uncovered queries emit a performance hint, if enabled. For example:

performance_hint: non_covered_document_read - .stock is not covered by the Product.sortedByPriceLowToHigh index. See https://docs.fauna.com/performance_hint/non_covered_document_read.
at *query*:1:42
  |
1 | Product.sortedByPriceLowToHigh() { name, stock }
  |                                          ^^^^^
  |

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’s values. For example:

collection Product {
  ...
  // Adds the `stock` field as an index value.
  index sortedByPriceLowToHigh {
    values [.price, .name, .description, .stock]
  }
}

Fauna does not return the non_covered_document_read performance hint for uncovered index queries that pass an mva() term.

No projection or mapping

Index queries without a projection or mapping are uncovered. Fauna must read each document returned in the Set. For example:

// This is an uncovered query.
// Queries without a projection or mapping
// require a document read.
Product.byName("limes")
Performance hint : non_covered_document_read

If performance hints are enabled, index queries without a projection or mapping emit a performance hint. For example:

performance_hint: non_covered_document_read - Full documents returned from Product.byName. See https://docs.fauna.com/performance_hint/non_covered_document_read.
at *query*:1:15
  |
1 | Product.byName("limes")
  |               ^^^^^^^^^
  |

If you frequently run such queries, consider adding the uncovered fields to the index definition’s values. For example:

collection Product {
  ...

  index byName {
    terms [.name]
    values [.price, .stock, .description]
  }

  ...
}

Then use projection or mapping to only return the fields you need. Given the previous index definition, the following query is covered:

// This is a covered query.
// `price`, `stock`, and `description` are values
// in the `byName()` index definition.
Product.byName("limes") {
  price,
  stock,
  description
}

Filter covered values

You can use set.where() to filter the results of an index call. If the set.where() predicate only accesses fields defined in the index definition’s terms and values, the query is covered.

For example, given the following index definition:

collection Product {
  ...

  index byName {
    terms [.name]
    values [.price, .description]
  }

  ...
}

The following query is covered:

// Covered query.
// Calls the `byName()` index.
// Uses `where()` to filter the results of
// the index call. The predicates only
// access covered terms and values.
Product.byName("limes")
  .where(.description.includes("Conventional"))
  .where(.price < 500) {
    name,
    description,
    price
  }

The following query is uncovered:

Product.byName("limes")
  .where(.description.includes("Conventional"))
  // The `where()` predicate accesses the uncovered
  // `stock` field.
  .where(.stock < 100)
  .where(.price < 500) {
    name,
    description,
    price
  }

To cover the query, add the uncovered field to the index definition’s values:

collection Product {
  ...

  index byName {
    terms [.name]
    // Adds `stock` to the index's values
    values [.price, .description, .stock]
  }

  ...
}

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!