🚀 Fauna Architectural Overview White Paper: Learn how Fauna's database engine scales with zero ops required
Download free
Fauna logo
Product
Solutions
Pricing
Resources
Company
Log InContact usStart for free
Fauna logo
Pricing
Customers
Log InContact usStart for free
© 0 Fauna, Inc. All Rights Reserved.

Related posts

Fauna’s 2024 Product RecapAnnouncing General Availability of Fauna SchemaFauna adds support for Change Data Capture

Start for free

Sign up and claim your forever-free Fauna account
Sign up and get started

Table of Contents

NEW
Dynamic queries

Building Flexible, Dynamic Filters with the Fauna Query Language

Paul Paterson|Jan 13th, 2025|

Categories:

RelationalFeaturesDatabase Insights
When building applications with the Fauna Query Language, you’ll often need to search through collections using multiple dynamic criteria. These criteria might include equality conditions, range queries, relationships, or custom logic. Managing these queries can quickly become complicated, especially when the list of search terms isn’t fixed and can grow or change on the fly.
In traditional database querying, you might end up with rigid or repetitive logic that doesn’t easily scale as your application’s complexity grows. Composition with Fauna’s FQL enables dynamic construction of queries—selecting indexes, filters, and conditions based on your current search terms—you can write code that’s both more flexible and easier to maintain.
This post explores a pattern for dynamically generating complex FQL. We’ll walk through some core examples, discuss performance considerations, and introduce a higher-level function that can piece together queries from a prioritized set of indexes and filters. By the end, you’ll have a template for managing complex, multi-criteria search without getting bogged down in repetitive, brittle code.

Problem Scenario

You have an application that needs to search through a collection with any number of search terms. These terms may involve equality checks, inequalities, verifying relationships, or other logic. While Fauna’s documentation offers guidance on working with multiple Sets, how do you scale this up to handle unknown, dynamic combinations of search criteria?

Some Examples with FQL

An intersection returns elements that exist in multiple Sets, similar to an INTERSECT clause in SQL. To perform intersections in FQL, start with the most selective expression and filter the resulting Sets using set.where().
For the best performance, use a covered index call as the first expression and only filter on covered values.
// Get the produce category.
let produce = Category.byName("produce").first()

// Use the most selective index, `byName()`, first.
// Then filter with covered values.
// In this example, `category`, `price`, and `name`
// should be covered by the `byName()` index.
Product.byName("limes")
  .where(.category == produce)
  .where(.price < 500) {
    name,
    category: .category!.name,
    price

Building with a Driver

So, how do we build queries like these dynamically at runtime? The Fauna client drivers compose queries using FQL template strings. You can interpolate variables, including other FQL template strings, into the template strings to compose dynamic queries.
Below is a simple JavaScript example that demonstrates how to compose queries. In the JS driver, you can use the fql tagged template function to create and reuse snippets:
let query = fql`Product.byName("limes")`

const filters = [
  fql`.where(doc => doc.category == Category.byName("produce").first()!)`,
  fql`.where(doc => doc.price < 500 )`
]

filters.forEach(filter => {
  query = fql`${query}${filter}`
})
Here, we start with a base query and then append filters dynamically. This pattern of interpolation makes it easy to add or remove conditions based on runtime logic.

Advanced Example

Let’s step up the complexity. Below is a TypeScript/JavaScript example that prioritizes certain indexes and filters when building queries based on a set of search terms. It allows you to define multiple strategies, ensuring that your query uses the most efficient approach available for any given search criterion.
/**
 * A Javascript object with a sorted list of indexes or filters.
 *
 * Javascript maintains key order for objects.
 * Sort items in the map from most to least selective.
 */
type QueryMap = Record<string, (...args: any[]) => Query>

/** Object to represent a search argument.
 *
 * Contains the name of the index to use and the arguments
 * to pass to it.
 *
 * Example:
 * { name: "by_name", args: ["limes"] }
 * { name: "range_price", args: [{ from: 100, to: 500 }] }
 */
type SearchTerm = {
  name: string
  args: any[]
}

/**
 * Composes a query by prioritizing the most selective index and then
 * applying filters.
 *
 * @param default_query - The initial query to which indexes and filters are applied.
 * @param index_map - A map of index names to functions that generate query components.
 * @param filter_map - A map of filter names to functions that generate query components.
 * @param search_terms - An array of search terms that specify the type and arguments
 *                       for composing the query.
 * @returns The composed query after applying all relevant indices and filters.
 */
const build_search = (
  default_query: Query,
  index_map: QueryMap,
  filter_map: QueryMap,
  search_terms: SearchTerm[]
): Query => {
  const _search_terms = [...search_terms]

  // Initialize a default query. Used if no other indexes are applicable.
  let query: Query = default_query

  // Iterate through the index map, from most to least selective.
  build_index_query: for (const index_name of Object.keys(
    index_map
  )) {
    // Iterate through each search term to check if it matches the highest priority index.
    for (const search_term of _search_terms) {
      // If a match is found, update the query. Then remove the search term from the
      // list and break out of the loop.
      if (index_name === search_term.name) {
        query = index_map[search_term.name](...search_term.args)
        _search_terms.splice(_search_terms.indexOf(search_term), 1)
        break build_index_query
      }
    }
  }

  // Iterate through the filter map, from most to least selective.
  for (const filter_name of Object.keys(filter_map)) {
    // Iterate through each search term to check if it matches the highest priority filter.
    for (const search_term of _search_terms) {
      // If a match is found, update the query. Then remove the search term from the list.
      if (filter_name === search_term.name) {
        const filter = filter_map[search_term.name](...search_term.args)
        query = fql`${query}${filter}`
        _search_terms.splice(_search_terms.indexOf(search_term), 1)
      }
    }
  }

  // If there are remaining search terms, you can't build the full query.
  if (_search_terms.length > 0) {
    throw new Error("Unable to build query")
  }

  return query
}
This function picks the most selective index first, then falls back to filters if needed. You define index_map and filter_map objects that map search criteria to FQL snippets, and build_search stitches them together based on the search terms provided.
An example:
// Implementation of `index_map` from the template.
// Sort items in the map from most to least selective.
const product_index_priority_map: QueryMap = {
  by_order: (id: string) =>
    fql`Order.byId(${id})!.items.map(.product!)`,
  by_name: (name: string) => fql`Product.byName(${name})`,
  by_category: (category: string) =>
    fql`Product.byCategory(Category.byName(${category}).first()!)`,
  range_price: (range: { from?: number; to?: number }) =>
    fql`Product.sortedByPriceLowToHigh(${range})`,
}

// Implementation of `filter_map` from the template.
// Sort items in the map from most to least selective.
const product_filter_map: QueryMap = {
  by_name: (name: string) => fql`.where(.name == ${name})`,
  by_category: (category: string) =>
    fql`.where(.category == Category.byName(${category}).first()!)`,
  range_price: ({ from, to }: { from?: number; to?: number }) => {
    // Dynamically filter products by price range.
    if (from && to) {
      return fql`.where(.price >= ${from} && .price <= ${to})`
    } else if (from) {
      return fql`.where(.price >= ${from})`
    } else if (to) {
      return fql`.where(.price <= ${to})`
    }
    return fql``
  },
}

const order_id = (await client.query(fql`Order.all().first()!`))
  .data.id

const query = build_search(
  fql`Product.all()`,
  product_index_priority_map,
  product_filter_map,
  [
    // { name: "name", args: ["limes"] },
    // { name: "category", args: ["produce"] },
    { name: "price", args: [{ to: 1000 }] },
    { name: "order", args: [order_id] },
  ]
)

const res = await client.query(query)
In this example, the code tries to find a relevant index first, then applies filters as needed. You can extend this logic to handle more complex scenarios, such as OR conditions or choosing the best index when multiple might apply.

Conclusion

By mixing and matching indexes and filters dynamically, you can create flexible, performant queries that adapt to changing search requirements. The code patterns shown here offer starting points for building your own dynamic query generators, ensuring your application can handle everything from a single straightforward filter to a complex matrix of search terms—all while taking advantage of Fauna’s powerful indexing and querying capabilities.
We’d love to hear your feedback and suggestions on extending this approach. Have you tried OR conditions, alternative indexing strategies, or integrated this approach into your build pipeline? Share your experiences on the Fauna Discord and let us know what’s worked best for your projects!
If you're just getting started with Fauna, sign-up for free and schedule a demo if you'd like to talk through your use case.

If you enjoyed our blog, and want to work on systems and challenges related to globally distributed systems, and serverless databases, Fauna is hiring

Share this post

Next ›︁

Subscribe to Fauna's newsletter

Get latest blog posts, development tips & tricks, and latest learning material delivered right to your inbox.