Sanity GROQ: Add fuzzy category/tag matching to search query

8 replies
Last updated: Nov 29, 2025
How can I improve this query? I know I am missing stuff (just not sure what it is), but I basically want to search and if the text matches a post's title or text in the body return it along with any services where the query matches the service title or description. However, I want to also add if a post or service is tagged or in a category that loosely matches the query... i.e. if someone searches "Food Bank", posts and services in the "Food Banks" category should still show up.
export const search = groq`*[
  (_type == "post" && (title match "*" + $query + "*" || pt::text(body) match "*" + $query + "*")) ||
  (_type == "service" && (title match "*" + $query + "*" || description match "*" + $query + "*"))
]{
  ...
}`
AI Update

Looking at your query, you're on the right track! Here are several improvements to help you implement the category/tag matching functionality you're looking for:

Use the in operator for array matching

The key to matching categories loosely (like "Food Bank" matching "Food Banks") is using the in operator to check if your search term exists within category/tag arrays:

export const search = groq`*[
  _type in ["post", "service"] && (
    // Text matching
    title match "*" + $query + "*" ||
    (defined(body) && pt::text(body) match "*" + $query + "*") ||
    (defined(description) && description match "*" + $query + "*") ||
    
    // Category matching (if categories is an array of references)
    $query in categories[]->title ||
    
    // Tag matching (if tags is an inline array)
    $query in tags[]
  )
]{
  _type,
  title,
  _type == "post" => {
    body,
    categories[]-> {
      title,
      slug
    }
  },
  _type == "service" => {
    description,
    categories[]-> {
      title
    }
  }
}

Improve relevance with scoring

To handle loose matching and rank results by relevance, use the score() function:

export const search = groq`*[
  _type in ["post", "service"] && (
    title match "*" + $query + "*" ||
    (defined(body) && pt::text(body) match "*" + $query + "*") ||
    (defined(description) && description match "*" + $query + "*") ||
    $query in categories[]->title ||
    $query in tags[]
  )
] | score(
  boost(title match "*" + $query + "*", 3),
  boost($query in categories[]->title, 2),
  boost(pt::text(body) match "*" + $query + "*", 1),
  boost(description match "*" + $query + "*", 1)
) | order(_score desc) {
  _type,
  _score,
  title,
  ...
}`

This scores exact title matches highest, category matches second, and body/description matches lower.

Handle the "Food Bank" → "Food Banks" fuzzy matching

The match operator tokenizes text, so "Food Bank" will match documents containing "Food" and "Bank" separately. However, for better category matching, you might want to:

  1. Normalize your search query - convert "Food Bank" to lowercase and check against lowercase category titles
  2. Use wildcards strategically - title match $query + "*" will match "Food Bank" to "Food Banks"
  3. Consider storing category slugs - make them searchable (e.g., "food-banks") for more reliable matching
*[
  _type in ["post", "service"] && (
    title match $query + "*" ||
    categories[]->slug.current match $query + "*" ||
    categories[]->title match $query + "*"
  )
]

Key improvements in your query:

  1. Combine document types - Use _type in ["post", "service"] instead of separate OR conditions
  2. Check for defined fields - Use defined(body) before accessing fields that might not exist
  3. Add category/tag filtering - Use $query in categories[]->title for reference arrays or $query in tags[] for inline arrays
  4. Add scoring - Rank results by relevance using the score() function
  5. Order results - Use order(_score desc) to show best matches first

The in operator is perfect for your use case - it checks if a value exists anywhere in an array, whether it's an inline array of strings or an array of references to other documents. This will help you match "Food Bank" queries to posts/services in the "Food Banks" category!

Show original thread
8 replies

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?