
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeI feel your pain! Searching through complex nested structures with lots of block types is definitely challenging. Unfortunately, GROQ doesn't currently have a built-in "walk" function that can automatically traverse arbitrary paths, but there are several strategies to make this more manageable.
Current Approaches:
pt::text() for standard Portable Text blocks: As you've discovered, pt::text() is great for extracting plain text from standard Portable Text structures. This handles the traversal automatically for those classic block types:*[_type == "post"] | score(pt::text(body) match $value)searchableText field whenever content changes:// In your sanity.blueprint.ts
export default {
name: 'search-indexer',
functions: [
{
name: 'index-searchable-text',
trigger: 'document.publish',
handler: async (event, context) => {
const doc = event.document;
const searchableText = extractAllText(doc.body); // Your custom extraction logic
await context.client.patch(doc._id)
.set({ searchableText })
.commit();
}
}
]
}Then your GROQ query becomes simply:
*[_type == "yourType"] | score(searchableText match $value) | order(_score desc)This approach with Sanity Functions is the modern, recommended way to handle this kind of automation - it runs on Sanity's infrastructure, scales automatically, and doesn't require external hosting like webhooks would.
[] operator: For your specific structure, you can use GROQ's array traversal. The match operator works with tokenized text:*[_type == "yourType"] | score(
body[_type == "diptych"].left[].title match $value,
body[_type == "diptych"].left[].surtitle match $value,
body[_type == "diptych"].left[].excerpt match $value,
body[_type == "diptych"].right[].title match $value,
body[_type == "process"].description match $value,
body[_type == "intro"].description match $value
) | order(_score desc)Regarding Your Proposed Solutions:
Custom walk function: This doesn't exist in GROQ currently, and you're right that it would be a language-level enhancement. This would be worth proposing in the Sanity GROQ GitHub discussions.
Custom GROQ functions under namespaces: Unfortunately, GROQ doesn't support user-defined functions or custom namespaces. The namespace functions like pt::text() are built into the GROQ engine itself.
Performance Considerations:
Multiple match operations in a score() function shouldn't cause major performance issues for moderately-sized datasets, but if you're worried about scaling, the "flatten at write time" approach with Sanity Functions is definitely more performant since you're doing the heavy lifting once when content changes rather than on every search query.
My Recommendation:
Use Sanity Functions to create a preprocessing step that flattens all searchable text into a dedicated field when documents are created or updated. This gives you:
You should definitely open a GitHub discussion about the walk function idea though - it's a legitimate use case that others would benefit from!
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store