How to group GROQ query results into separate arrays by _type?

20 replies
Last updated: May 18, 2023
I’d like to put my results into separate arrays by _type. How would I do that? Current GROQ is:
[_type in ["product", "set", "collection", "room", "post"] && [store.title, title] match "${params.s}"] {
    _type == 'post' => {_id, _type, 'image': image.asset->url, title, 'slug': slug.current},
    _type == 'room' => {_id, _type, 'image': image.asset->url, title, 'slug': slug.current},
    _type == 'collection' => {_id, _type, 'image': image.asset->url, title, 'slug': slug.current},
    _type == 'set' => {_id, _type, 'image': image.asset->url, title, 'slug': slug.current}, 
    _type == 'product' => {_type, primary->, 'shopifyId': store.id, 'image': store.previewImageUrl, 'slug': store.slug.current, 'title': store.title }}
AI Update

Based on your GROQ query, you want to group search results by document type into separate arrays. GROQ doesn't have a built-in GROUP BY operation like SQL, but you can achieve this by running separate filtered queries for each type and combining them into an object with arrays.

Here's how to restructure your query:

{
  "products": *[_type == "product" && [store.title, title] match "${params.s}"] {
    _type, 
    primary->, 
    'shopifyId': store.id, 
    'image': store.previewImageUrl, 
    'slug': store.slug.current, 
    'title': store.title
  },
  "sets": *[_type == "set" && [store.title, title] match "${params.s}"] {
    _id, 
    _type, 
    'image': image.asset->url, 
    title, 
    'slug': slug.current
  },
  "collections": *[_type == "collection" && [store.title, title] match "${params.s}"] {
    _id, 
    _type, 
    'image': image.asset->url, 
    title, 
    'slug': slug.current
  },
  "rooms": *[_type == "room" && [store.title, title] match "${params.s}"] {
    _id, 
    _type, 
    'image': image.asset->url, 
    title, 
    'slug': slug.current
  },
  "posts": *[_type == "post" && [store.title, title] match "${params.s}"] {
    _id, 
    _type, 
    'image': image.asset->url, 
    title, 
    'slug': slug.current
  }
}

This will return an object like:

{
  "products": [...],
  "sets": [...],
  "collections": [...],
  "rooms": [...],
  "posts": [...]
}

Why this approach works: In GROQ, you can create custom object projections where each property contains its own query. Each *[...] query runs independently and returns an array of matching documents.

Alternative client-side approach: If you prefer to keep a single query (for performance reasons or to maintain a single sort order across types), you could stick with your original query and group the results in your application code:

const results = await client.fetch(yourOriginalQuery);
const grouped = results.reduce((acc, item) => {
  const type = item._type;
  if (!acc[type]) acc[type] = [];
  acc[type].push(item);
  return acc;
}, {});

The GROQ approach is cleaner if you want the grouping done server-side, while the client-side approach works better if you need cross-type sorting before grouping.

Show original thread
20 replies
It may be more performant to wrap your queries in an object instead of using a conditional within a single projection. Before I put together an example, it looks like all types have the same project accept for a product. Is that right?
yes
I have noticed this query takes longer
Ok cool, I’d do something like this:
const query = {};

['product', 'set', 'collection', 'room', 'post'].forEach(type =>
  Object.assign(
    query,
    type !== 'product'
      ? {
          [type]: `*[_type == '${type}' && [store.title, title] match $s]{_id, _type, 'image': image.asset->url, title, 'slug': slug.current}`,
        }
      : {
          product: `*[_type == 'product'] {_type, primary->, 'shopifyId': store.id, 'image': store.previewImageUrl, 'slug': store.slug.current, 'title': store.title }]`,
        }
  )
);

await client.fetch(query, {s})
Filtering this way is much faster than using conditionals in a project. You also don’t have to repeat the projection a bunch of times inside of your code.
I’m not using the client
let sanityPromise;

// Sanity API Call
async function sanityApiCall(query) {
    try {
      let response = await fetch(`<https://umt44hrc.api.sanity.io/v2022-01-01/data/query/production?query=*${query}>`)
      sanityPromise = await response.json()
      sanityPromise = sanityPromise.result
    } catch (error) {
      topBannerStart('error', error);
    }
  }

function searchSanity() {
    let query = encodeURIComponent(`[_type in ["product", "set", "collection", "room", "post"] && [store.title, title] match "${params.s}"] {${searchProjection}} | order(_type desc)`);
    sanityApiCall(query).then(res => {
        console.log(sanityPromise)
        sanityPromise.forEach((line)=>{
            listResults(line)
        });
        if(sanityPromise.length == 0) {
            let message = document.createElement('p');
            message.innerHTML = "Looks like there's nothing here!";
            message.setAttribute('class', 'default-message')
            results.append(message);
        };
    });
    
}
the search function is only for the search page. I use the sanity api call elsewhere, but not searchSanity
so do I put your code as the query in searchSanity?
because it looks like the GROQ is mixed with JS, and I’ve not seen that before
I can always mutate the result into arrays, but I’d like to make a more performant query regardless
user M
I was wondering if you had any advice for me? Thank you so much 🙏🙏🙏
Should I do separate fetches, one for each _type?
Yeah, I’m using JS here to save having to repeat the projection for each query. Try something like this:
let sanityPromise;

// Sanity API Call
async function sanityApiCall(query) {
    try {
      let response = await fetch(`<https://umt44hrc.api.sanity.io/v2022-01-01/data/query/production?query=*${query}>`)
      sanityPromise = await response.json()
      sanityPromise = sanityPromise.result
    } catch (error) {
      topBannerStart('error', error);
    }
  }

function searchSanity() {
    const query = {};

['product', 'set', 'collection', 'room', 'post'].forEach(type =>
  Object.assign(
    query,
    type !== 'product'
      ? {
          [type]: `*[_type == '${type}' && [store.title, title] match ${params.s}]{_id, _type, 'image': image.asset->url, title, 'slug': slug.current}`,
        }
      : {
          product: `*[_type == 'product'] {_type, primary->, 'shopifyId': store.id, 'image': store.previewImageUrl, 'slug': store.slug.current, 'title': store.title }]`,
        }
  )
);
    sanityApiCall(query).then(res => {
        console.log(sanityPromise)
        sanityPromise.forEach((line)=>{
            listResults(line)
        });
        if(sanityPromise.length == 0) {
            let message = document.createElement('p');
            message.innerHTML = "Looks like there's nothing here!";
            message.setAttribute('class', 'default-message')
            results.append(message);
        };
    });
    
}
You may have to tweak this though, since I’m not 100% sure where your params are coming from.
Wonderful, I'll give that a try. Afterwards I may have a question for you if I don't understand how it works.
Once again, you are an absolute GROQ magician
💜
Let me know how it goes!
Will do.
Okay, I took your code and ran with it a bit. Here is what I have now:
function searchSanity() {
    let query
    ['product', 'set', 'collection', 'room', 'post'].forEach(type => {
        switch (type) {
            case 'product': query = `[_type == 'product' && [store.title, title] match "${params.s}"] {_type, primary->, 'shopifyId': store.id, 'image': store.previewImageUrl, 'slug': store.slug.current, 'title': store.title }`
            break;
            default: query = `[_type == '${type}' && [store.title, title] match "${params.s}"]{_id, _type, 'image': image.asset->url, title, 'slug': slug.current}`
            break;
        }
        console.log(query)
        // Query sanity api for each type and list the results in div
        sanityApiCall(encodeURIComponent(query)).then(res => {
            if(res[0]) {
                console.log(res)
                div = document.getElementById(type);
                div.innerHTML = `<h4>${type}s</h4>`
                // Display each item of the given type
                res.forEach((line)=>{
                    listResults(line,type);
                });
            }
            
        }); 
    });
    
}

// Sanity API Call
async function sanityApiCall(query) {
    try {
      let response = await fetch(`<https://umt44hrc.api.sanity.io/v2022-01-01/data/query/production?query=*${query}>`)
      sanityPromise = await response.json()
      sanityPromise = sanityPromise.result
      return sanityPromise;
    } catch (error) {
      topBannerStart('error', error);
    }
  }
so each _type goes into a separate div with that ID, so I can do things like the screenshot attached!
Awesome! Glad you got it working!

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?