Improving a GROQ query for searching posts and services

8 replies
Last updated: May 3, 2023
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!

user T
sorry for the ping but I have a demo of this website coming up today/tomorrow and was curious if you had any insight or know anyone that does on something like this :gratitude-thank-you:
Hey User, not quite sure sorry. Building a search with GROQ is among the things I have to raise my hand and say I’ve never done!
Think I’d start by intercepting the network requests when using search in the Studio for some pointers. Wish I had a better answer than that!
How can I go about intercepting that?
Network tab in chrome inspect
And that shows the groq query? Cool
Kinda, it's pretty hard to interpret, I'm sure it's possible
So I think I figured it out
export const search = groq`*[
  (_type == "post" && (title match "*" + $query + "*" || pt::text(body) match "*" + $query + "*" || tags[]->title match "*" + $query + "*" || categories[]->title match "*" + $query + "*")) ||
  (_type == "service" && (title match "*" + $query + "*" || description match "*" + $query + "*" || tags[]->title match "*" + $query + "*" || categories[]->title match "*" + $query + "*"))
]{
  ...,
  _type == "post" => {
    ${postFields}
  },
}`
Kind of messy but
🤷
On a lark I looked at some of the queries for my studio when using the actual built-in search feature, and at least to my layperson eye it doesn't seem enormously different, so take comfort in that! 🙂

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?