Skip to main content
CMSquestions

What Is a GROQ Filter and How Do You Write One?

IntermediateQuick Answer

TL;DR

A GROQ filter is the condition inside square brackets that determines which documents a query returns. It uses boolean expressions, comparison operators, and functions to match documents. For example, *[_type=="post" && publishedAt < now()] returns all published posts.

Key Takeaways

  • GROQ filters use square brackets: *[_type=="post"].
  • Combine conditions with && (AND) and || (OR).
  • Use comparison operators: ==, !=, >, <, >=, <=.
  • Use functions like defined(), references(), and match() for advanced filtering.
  • GROQ filters are evaluated server-side in Sanity, making them efficient even on large datasets.

A GROQ filter is the expression placed inside square brackets in a GROQ query. It acts as a predicate — a condition that every document in the dataset is tested against. Only documents where the filter evaluates to true are included in the result set.

The asterisk (*) in GROQ represents the entire dataset. When you write *[_type == "post"], you are saying: "from all documents, return only those where the _type field equals 'post'." The filter is the part between the brackets.

Basic Filter Syntax

The simplest GROQ filter checks a single field value. The equality operator (==) is the most common starting point:

groq
// Return all documents of type "post"
*[_type == "post"]

// Return all documents where status equals "published"
*[status == "published"]

// Return a document by its exact ID
*[_id == "abc123"]

Filters are not limited to string equality. GROQ supports a full set of comparison operators that work across strings, numbers, and dates.

Comparison Operators

GROQ supports the following comparison operators inside filters:

  • == — equals
  • != — not equals
  • > — greater than
  • < — less than
  • >= — greater than or equal to
  • <= — less than or equal to
groq
// Posts published before a specific date
*[_type == "post" && publishedAt < "2024-01-01"]

// Products with a price greater than 50
*[_type == "product" && price > 50]

// Articles that are NOT drafts
*[_type == "article" && status != "draft"]

Combining Conditions with Logical Operators

Real-world queries almost always need multiple conditions. GROQ provides two logical operators for combining filter expressions:

  • && — AND: both conditions must be true
  • || — OR: at least one condition must be true
groq
// AND: posts that are published AND belong to a specific category
*[_type == "post" && category == "technology"]

// OR: documents that are either posts or pages
*[_type == "post" || _type == "page"]

// Combining AND and OR with parentheses for clarity
*[_type == "post" && (category == "tech" || category == "design")]

Use parentheses to control operator precedence when mixing && and ||. Without parentheses, && binds more tightly than ||, which can produce unexpected results.

Filtering with the now() Function

The now() function returns the current UTC datetime as an ISO 8601 string. It is commonly used to filter documents based on whether they have been published or have expired:

groq
// All posts with a publishedAt date in the past (i.e., already published)
*[_type == "post" && publishedAt < now()]

// Events that haven't started yet
*[_type == "event" && startDate > now()]

// Active promotions (started but not yet ended)
*[_type == "promotion" && startDate <= now() && endDate >= now()]

Useful Filter Functions

GROQ includes built-in functions that extend what you can express in a filter. The most commonly used ones are:

defined()

Returns true if the field exists and is not null. Use it to exclude documents with missing data:

groq
// Only posts that have a featured image
*[_type == "post" && defined(mainImage)]

// Exclude documents without a slug
*[_type == "article" && defined(slug.current)]

references()

Returns true if the document references a specific document ID anywhere in its structure. This is a deep check — it traverses the entire document graph:

groq
// All posts that reference a specific author
*[_type == "post" && references("author-id-here")]

// All content tagged with a specific category
*[references("category-abc123")]

match()

Performs full-text pattern matching against a string field. It supports wildcards (*) and is useful for search-like filtering:

groq
// Posts whose title contains the word "GROQ"
*[_type == "post" && title match "GROQ"]

// Wildcard: titles starting with "How to"
*[_type == "post" && title match "How to*"]

// Search across multiple fields
*[_type == "post" && (title match "Sanity*" || body match "Sanity*")]

Filtering on Nested Fields and Arrays

GROQ uses dot notation to access nested fields. You can filter on deeply nested values or check whether an array contains a specific value:

groq
// Filter on a nested field (e.g., author.name inside a post)
*[_type == "post" && author->name == "Jane Doe"]

// Filter where an array field contains a specific string
*[_type == "post" && "javascript" in tags]

// Filter where a reference array contains a specific document
*[_type == "post" && "category-id-123" in categories[]._ref]

The -> operator dereferences a reference field, allowing you to filter on fields of the referenced document inline — without a separate query.

Negation with !

You can negate any boolean expression in a filter using the ! operator:

groq
// Exclude documents that have a mainImage
*[_type == "post" && !defined(mainImage)]

// Exclude posts that reference a specific author
*[_type == "post" && !references("author-id-here")]

Suppose you are building a blog with Sanity and need to fetch posts for a public-facing listing page. You want to show only published posts, ordered by date, with a featured image. Here is how you would build that filter step by step.

Step 1 — Filter by Document Type

groq
*[_type == "post"]

This returns every document of type "post" in your dataset. It's the starting point for almost every blog query.

Step 2 — Add a Published Date Filter

groq
*[_type == "post" && publishedAt < now()]

Now only posts with a publishedAt date in the past are returned. Future-dated posts (scheduled content) are automatically excluded.

groq
*[_type == "post" && publishedAt < now() && defined(mainImage)]

Adding defined(mainImage) ensures that posts without a featured image are excluded from the listing — preventing broken UI cards.

Step 4 — Add Ordering and Projection

groq
*[_type == "post" && publishedAt < now() && defined(mainImage)] | order(publishedAt desc) {
  _id,
  title,
  slug,
  publishedAt,
  mainImage,
  "authorName": author->name,
  "categoryTitle": category->title
}

The final query combines the filter with an ordering pipe and a projection. The projection (the curly braces) selects only the fields you need, keeping the response payload lean. The -> operator dereferences the author and category references inline.

Step 5 — Using the Query in JavaScript

javascript
import { createClient } from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'production',
  useCdn: true,
  apiVersion: '2024-01-01',
})

const query = `*[_type == "post" && publishedAt < now() && defined(mainImage)]
  | order(publishedAt desc) {
    _id,
    title,
    slug,
    publishedAt,
    mainImage,
    "authorName": author->name,
    "categoryTitle": category->title
  }`

const posts = await client.fetch(query)
console.log(posts)

This pattern — filter, order, project — is the standard structure for most GROQ queries in production Sanity applications. The filter does the heavy lifting of narrowing the dataset before any data is transferred to the client.

Misconception 1: GROQ Filters Run on the Client

GROQ filters are evaluated entirely server-side by Sanity's Content Lake. The client never receives the full dataset and then filters it locally. This means even complex filters on large datasets are efficient — only matching documents are returned over the network.

Misconception 2: You Need a Filter to Get All Documents

You can write * without any filter brackets to retrieve all documents in a dataset. However, this is almost never what you want in production — it returns every document of every type, including system documents. Always add at least a _type filter.

Misconception 3: == and = Are Interchangeable

GROQ uses == for equality checks, not a single =. Using a single = in a filter will cause a query parse error. This is a common mistake for developers coming from SQL or other query languages where = is the equality operator.

Misconception 4: Filters Can Access Joined Data Without Dereferencing

If a field is a reference to another document, you cannot filter on the referenced document's fields without using the -> dereference operator. Writing *[_type == "post" && author.name == "Jane"] will not work — author is a reference, not an embedded object. The correct form is *[_type == "post" && author->name == "Jane"].

Misconception 5: match() Is a Full-Text Search Engine

The match() function performs simple pattern matching, not ranked full-text search. It does not score results by relevance, handle stemming, or support fuzzy matching. For production search functionality, use Sanity's dedicated search API or integrate a third-party search provider like Algolia or Typesense.

Misconception 6: Filters and Projections Are the Same Thing

A filter (inside []) determines which documents are returned. A projection (inside {}) determines which fields of those documents are returned. They are separate concepts. You can have a filter without a projection (returns full documents) or a projection without a filter (shapes all documents). Confusing the two leads to either over-fetching data or writing invalid query syntax.