Fri Jun 30 2023

A simple text search using Sanity and groq

This post describes a way to use groq to search Sanity for content. This includes searching simple strings and portable text fields, then sorting the results by using the score and boost functions in groq.

PS: This is a simple search using only groq, a query language. It doesn't handle spelling errors, synonyms and other stuff you might find in a dedicated search engine.

The document

Lets say that we have the following schema for an article.

export default {
    name: "article",
    title: "Article",
    type: "document",
    fields: [
            name: "title",
            title: "Title",
            type: "string",
            validation: (Rule) => Rule.required(),
            name: "description",
            title: "Description",
            type: "text",
            validation: (Rule) => Rule.required(),
            name: "body",
            title: "Body",
            type: "blockContent",

The goal

Let's say we want to search the title, description and the body. We want hits to be ranked in such a way that a hit in the title should be ranked above the description, and the description above the body text.

Defining the search

This is how the full query could look.

import groq from "groq";

 // Get this from a state or a searchParam...
const query = "the search query";

// The full groq query
const groqQuery = groq`*[(_type == "article" && !(_id in path("drafts.**"))
	&& (pt::text(body) match "${query}*" || title match "${query}*" || description match "${query}*")] 
	| score(pt::text(body) match "${query}*", boost(title match "${query}*", 3), boost(description match "${query}*", 2))
		title, description, body,

Let's take this step by step to make it more understandable.

// Get all documents of type article
// except drafts
*[(_type == "article" && !(_id in path("drafts.**"))
&& // and
// Try to find a word in the text version
// of the body, that starts with the search query
(pt::text(body) match "${query}*" 
|| // or
// The same as above, but for the title
title match "${query}*" 
|| // or
// The same as above, but for the description
description match "${query}*")]
| // Pipe
// Add a score to each of the fields
// based on the frequency of matches per expression
  // The body does not get any boost
  // since we rank this the lowest
  pt::text(body) match "${query}*",
  // We boost the score of the title by 3 
  // (most important)
  boost(title match "${query}*", 3),
  // We boost the score of the description by 2
  // (second most important)
  boost(description match "${query}*", 2)
) {
  // the fields to return
  title, description, body, _score

Using the result, you can filter out documents with a score of 0 and the sort the resulting array of documents using the _score field. You can also do the filtering and sorting in the query itself.

As stated at the top, this doesn't replace a dedicated search engine, but it might work for smaller project or situations where you are selecting documents rather than doing a full text search.