Improving a GROQ query for searching posts and services
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:
- Normalize your search query - convert "Food Bank" to lowercase and check against lowercase category titles
- Use wildcards strategically -
title match $query + "*"will match "Food Bank" to "Food Banks" - 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:
- Combine document types - Use
_type in ["post", "service"]instead of separate OR conditions - Check for defined fields - Use
defined(body)before accessing fields that might not exist - Add category/tag filtering - Use
$query in categories[]->titlefor reference arrays or$query in tags[]for inline arrays - Add scoring - Rank results by relevance using the
score()function - 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!
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.