Sanity 쿼리 치트시트
Sanity 쿼리 치트시트
GROQ(Graph-Relational Object Queries)는 Sanity의 콘텐츠 레이크에서 데이터를 뽑아내는 쿼리 언어예요. *[_type == "movie"] 같은 아주 짧은 한 줄이 필터링과 프로젝션, 정렬, 심지어 참조를 따라가며 가져오는 조인까지 전부 처리해요. 목록에서 문서 하나당 타이틀만 뽑고 싶을 때, 특정 장르의 영화만 골라 최신순으로 정렬하고 싶을 때 — 이 치트시트의 예시들을 그대로 가져다 쓰면 대부분 바로 해결돼요.
쿼리는 콘텐츠 레이크의 HTTP 엔드포인트에 직접 호출하거나, JavaScript·PHP SDK, Sanity Studio 안에서 실행하는 Vision 플러그인, 그리고 아무 JSON 데이터셋으로 쿼리를 돌려볼 수 있는 groq.dev에서 실행할 수 있어요. 쿼리가 예상대로 안 나오면 API 버전(API versioning), 퍼스펙티브(perspectives), 필터 쪽을 먼저 확인해 보세요. 그리고 기억할 유용한 팁 하나 — 요청한 키가 실제로 없으면 쿼리 결과가 null이 돼요. 그래서 key != null로 필터하면 그 값이 실제로 존재하는지 확인할 수 있어요.
출처: 문서
본문
실행 방법
GROQ 쿼리를 실행하는 방법은 몇 가지가 있어요.
- 콘텐츠 레이크의 쿼리 HTTP 엔드포인트(HTTP endpoint)에 직접 호출
- JavaScript·PHP SDK 또는 다른 클라이언트(another client) 사용
- Sanity Studio 안에서 쿼리를 바로 실행하는 Vision 플러그인 설치
- 아무 JSON 데이터셋으로 쿼리를 돌려보는 groq.dev
쿼리를 HTTP 엔드포인트에 보낼 때는 query 파라미터로 쿼리 문자열을 전달해요. 예를 들어 모든 영화 문서를 가져오려면 다음과 같이 호출하면 돼요.
curl -g 'https://<projectId>.api.sanity.io/v2021-10-21/data/query/production?query=*[_type%20==%20"movie"]'
필터 (Filters)
GROQ 필터는 * 다음 [ ... ] 괄호 안에 조건을 쓰는 방식이에요. 문서 컬렉션 전체를 다루는 *를 기준으로, 각종 비교 연산자를 이용해 원하는 문서를 골라내요.
* // Everything, i.e. all documents
*[] // Everything with no filters applied, i.e. all documents
*[_type == "movie"] // All movie documents
*[_id == "abc.123"] // _id equals
*[_type in ["movie", "person"]] // _type is movie or person
*[_type == "movie" && popularity > 15 && releaseDate > "2016-04-25"] // multiple filters AND
*[_type == "movie" && (popularity > 15 || releaseDate > "2016-04-25")] // multiple filters OR
*[popularity < 15] // less than
*[popularity > 15] // greater than
*[popularity <= 15] // less than or equal
*[popularity >= 15] // greater than or equal
*[popularity == 15]
*[releaseDate != "2016-04-27"] // not equal
*[!(releaseDate == "2016-04-27")] // not equal
*[!(releaseDate != "2016-04-27")] // even equal via double negatives "not not equal"
*[dateTime(_updatedAt) > dateTime('2018-04-20T20:43:31Z')] // Use zulu-time when comparing datetimes to strings
*[dateTime(_updatedAt) > dateTime(now()) - 60*60*24*7] // Updated within the past week
*[name < "Baker"] // Records whose name precedes "Baker" alphabetically
*[awardWinner == true] // match boolean
*[awardWinner] // true if awardWinner == true
*[!awardWinner] // true if awardWinner == false
*[defined(awardWinner)] // has been assigned an award winner status (any kind of value)
*[!defined(awardWinner)] // has not been assigned an award winner status (any kind of value)
*[title == "Aliens"]
*[title in ["Aliens", "Interstellar", "Passengers"]]
*[_id in path("a.b.c.*")] // _id matches a.b.c.d but not a.b.c.d.e
*[_id in path("a.b.c.**")] // _id matches a.b.c.d, and also a.b.c.d.e.f.g, but not a.b.x.1
*[!(_id in path("a.b.c.**"))] // _id matches anything that is not under the a.b.c path or deeper
*["yolo" in tags] // documents that have the string "yolo" in the array "tags"
*[status in ["completed", "archived"]] // the string field status is either == "completed" or "archived"
*["person_sigourney-weaver" in castMembers[].person._ref] // Any document having a castMember referencing sigourney as its person
*[slug.current == "some-slug"] // nested properties
*[count((categories[]->slug.current)[@ in ["action", "thriller"]]) > 0] // documents that reference categories with slugs of "action" or "thriller"
*[count((categories[]->slug.current)[@ in ["action", "thriller"]]) == 2] // documents that reference categories with slugs of "action" and "thriller". set == 2 based on the total number of items in the array
*[sanity::dataset() == 'production'] // compare dataset where query is run. Useful in webhooks or functions
*[sanity::dataset() in ['prod', 'staging', 'next']] // compare dataset name with list
*[string::startsWith(sanity::dataset(), 'prod_')] // check for prefixed dataset names, such as prod_marketing, prod_cs, etc.
*[_type == "movie" && genre == user::attributes().genre] // All movie documents that have a genre that matches the genre attribute on the current user (premium feature)
텍스트 매칭 (Text matching)
match 연산자는 사람이 쓰는 자연어 텍스트를 위한 것이어서, ==처럼 정확히 같음을 비교하지는 않아요. 와일드카드 *로 부분 일치를 표현할 수 있고, 토큰 단위로 동작한다는 점에 주의하세요.
// Text contains the word "word"
*[text match "word"]
// Title contains a word starting with "wo"
*[title match "wo*"]
// Inverse of the previous query; animal matches the start of the word "caterpillar" (perhaps animal == "cat")
*["caterpillar" match animal + "*"]
// Title and body combined contains a word starting with "wo" and the full word "zero"
*[[title, body] match ["wo*", "zero"]]
// Are there aliens in my rich text?
*[body[].children[].text match "aliens"]
// Note how match operates on tokens!
"foo bar" match "fo*" // -> true
"my-pretty-pony-123.jpg" match "my*.jpg" // -> false
슬라이스 (Slice operations)
슬라이스는 결과에서 특정 구간을 잘라내는 기능이에요. 기본적으로는 제한이 없어서, 슬라이스를 명시하지 않으면 전부를 가져와요. 아주 큰 결과 집합은 API의 실행 시간·작업 집합 제한에 걸릴 수 있으니 주의하고요.
*[_type == "movie"][0] // a single movie (an object is returned, not an array)
*[_type == "movie"][0..5] // first 6 movies (inclusive)
*[_type == "movie"][0...5] // first 5 movies (non-inclusive)
*[_type == "movie"]{title}[0...10] // first 10 movie titles
*[_type == "movie"][0...10]{title} // first 10 movie titles
*[_type == "movie"][10...20]{title} // first 10 movie titles, offset by 10
*[_type == "movie"] // no slice specified --> all movies are returned
참고: 위 쿼리들은 정렬(ordering) 없이는 큰 의미가 없어요. 예를 들어 "첫 6편의 영화"는 백엔드가 우연히 먼저 뽑아낸 6편일 뿐이니까요.
정렬 (Ordering)
기본적으로 문서는 _id 오름차순으로 반환돼요. 부분 집합을 조회할 때는 정렬을 명시해 주는 게 좋아요. 단, 어떤 정렬 순서를 지정하든 _id 오름차순이 항상 최종 tie-breaker로 남는다는 점도 기억해 두세요. GROQ에는 랜덤 정렬 내장 함수가 없어서, 랜덤으로 보여주고 싶다면 표준 쿼리로 가져온 뒤 앱 코드에서 섞는 방식을 써요.
// order results
*[_type == "movie"] | order(_createdAt asc)
// order results by multiple attributes
*[_type == "movie"] | order(releaseDate desc) | order(_createdAt asc)
// order todo items by descending priority,
// where priority is equal, list most recently updated
// item first
*[_type == "todo"] | order(priority desc, _updatedAt desc)
// the single, oldest document
*[_type == "movie"] | order(_createdAt asc)[0]
// the single, newest document
*[_type == "movie"] | order(_createdAt desc)[0]
// oldest 10 documents
*[_type == "movie"] | order(_createdAt asc)[0..9]
// BEWARE! This selects 10 documents using the default
// ordering, and *only the selection* is ordered by
// _createdAt in ascending order
*[_type == "movie"][0..9] | order(_createdAt asc)
// order results alphabetically by a string field
// This is case sensitive, so A-Z come before a-z
*[_type == "movie"] | order(title asc)
// order results alphabetically by a string field,
// ignoring case
*[_type == "movie"] | order(lower(title) asc)
조인 (Joins)
->는 참조(reference)를 따라가서 대상 문서의 필드를 가져오는 dereference 연산자예요. 참조를 배열 원소에 잇따라 적용하거나, ^ 연산자로 바깥 문서를 다시 참조하는 패턴도 사용해요.
// Fetch movies with title, and join with poster asset with path + url
*[_type=='movie']{title,poster{asset->{path,url}}}
// Say castMembers is an array containing objects with character name and a reference to the person:
// We want to fetch movie with title and an attribute named "cast" which is an array of actor names
*[_type=='movie']{title,'cast': castMembers[].person->name}
// Same query as above, except "cast" now contains objects with person._id and person.name
*[_type=='movie']{title,'cast': castMembers[].person->{_id, name}}
// Using the ^ operator to refer to the enclosing document. Here ^._id refers to the id
// of the enclosing person record.
*[_type=="person"]{
name,
"relatedMovies": *[_type=='movie' && references(^._id)]{ title }
}
// Books by author.name (book.author is a reference)
*[_type == "book" && author._ref in *[_type=="author" && name=="John Doe"]._id ]{...}
객체와 배열 (Objects and arrays)
여러 개의 독립적인 쿼리를 한 요청에 묶거나, 배열 유틸리티 함수로 배열을 가공할 수도 있어요.
// Create your own objects
// https://groq.dev/lcGV0Km6dpvYovREqq1gLS
{
// People ordered by Nobel prize year
"peopleByPrizeYear": *[]|order(prizes[0].year desc){
"name": firstname + " " + surname,
"orderYear": prizes[0].year,
prizes
},
// List of all prizes ordered by year awarded
"allPrizes": *[].prizes[]|order(year desc)
}
// Get all Nobel prizes from all root person documents
// https://groq.dev/v8T0DQawC6ihbNUf4cUeeS
*[].prizes[]
array::join(tags, ", ") // tags = ["Rust", "Go", null, "GROQ"] => "Rust, Go, <INVALID>, GROQ"
array::join(["a", "b", "c"], ".") // "a.b.c"
array::join(year, ".") // year = 2024 => null (not an array)
array::join(values, 1) // values = [10, 20, 30] => null (separator must be a string)
array::compact(numbers) // numbers = [1, null, 2, null, 3] => [1, 2, 3]
array::unique(items) // items = [1, 2, 2, 3, 4, 5, 5] => [1, 2, 3, 4, 5]
array::unique(records) // records = [[1], [1]] => [[1], [1]] (arrays are not comparable)
array::intersects(firstList, secondList) // firstList = [1, 2, 3], secondList = [3, 4, 5] => true
array::intersects(tags, keywords) // tags = ["tech", "science"], keywords = ["art", "design"] => false
객체 프로젝션 (Object projections)
프로젝션은 돌려받을 필드를 선택하는 부분이에요. { title }처럼 뽑을 필드를 나열하고, '별명': 필드 형태로 이름을 바꿔줄 수 있어요. ...는 문서의 모든 속성을 펼치는 전개 연산자고요.
// return only title
*[_type == 'movie']{title}
// return values for multiple attributes
*[_type == 'movie']{_id, _type, title}
// explicitly name the return field for _id
*[_type == 'movie']{'renamedId': _id, _type, title}
// Return an array of attribute values (no object wrapper)
*[_type == 'movie'].title
*[_type == 'movie']{'characterNames': castMembers[].characterName}
// movie titled Arrival and its posterUrl
*[_type=='movie' && title == 'Arrival']{title,'posterUrl': poster.asset->url}
// Explicitly return all attributes
*[_type == 'movie']{...}
// Some computed attributes, then also add all attributes of the result
*[_type == 'movie']{'posterUrl': poster.asset->url, ...}
// Default values when missing or null in document
*[_type == 'movie']{..., 'rating': coalesce(rating, 'unknown')}
// Number of elements in array 'actors' on each movie
*[_type == 'movie']{"actorCount": count(actors)}
// Apply a projection to every member of an array
*[_type == 'movie']{castMembers[]{characterName, person}}
// Filter embedded objects
*[_type == 'movie']{castMembers[characterName match 'Ripley']{characterName, person}}
// Follow every reference in an array of references
*[_type == 'book']{authors[]->{name, bio}}
// Explicity name the outer return field
{'threeMovieTitles': *[_type=='movie'][0..2].title}
// Combining several unrelated queries in one request
{'featuredMovie': *[_type == 'movie' && title == 'Alien'][0], 'scifiMovies': *[_type == 'movie' && 'sci-fi' in genres]}
특수 변수 (Special variables)
*— 모든 문서, 즉 현재 스코프의 루트 컬렉션@— 현재 처리 중인 값(문서나 배열 원소), 스코프의 루트를 가리켜요^— 바깥(둘러싸는) 문서를 가리켜요
// *
* // Everything, i.e. all documents
// @
*[ @["1"] ] // @ refers to the root value (document) of the scope
*[ @[$prop]._ref == $refId ] // Select reference prop from an outside variable.
*{"arraySizes": arrays[]{"size": count(@)}} // @ also works for nested scopes
// ^
// ^ refers to the enclosing document. Here ^._id refers to the id
// of the enclosing person record.
*[_type=="person"]{
name,
"relatedMovies": *[_type=='movie' && references(^._id)]{ title }
}
조건문 (Conditionals)
select()는 왼쪽 조건이 참이 되는 첫 => 쌍을 반환해요. 첫 번째 파라미터에 =>가 없으면 이전 매칭이 없을 때 반환되는 기본값이 돼요. 프로젝션 안에서는 인라인 조건부 문법(조건 => { ... })도 바로 쓸 수 있어요.
// select() returns the first => pair whose left-hand side evaluates to true
*[_type=='movie']{..., "popularity": select(
popularity > 20 => "high",
popularity > 10 => "medium",
popularity <= 10 => "low"
)}
// The first select() parameter without => is returned if no previous matches are found
*[_type=='movie']{..., "popularity": select(
popularity > 20 => "high",
popularity > 10 => "medium",
"low"
)}
// Projections also have syntactic sugar for inline conditionals
*[_type=='movie']{
...,
releaseDate >= '2018-06-01' => {
"screenings": *[_type == 'screening' && movie._ref == ^._id],
"news": *[_type == 'news' && movie._ref == ^._id],
},
popularity > 20 && rating > 7.0 => {
"featured": true,
"awards": *[_type == 'award' && movie._ref == ^._id],
},
}
// The above is exactly equivalent to:
*[_type=='movie']{
...,
...select(releaseDate >= '2018-06-01' => {
"screenings": *[_type == 'screening' && movie._ref == ^._id],
"news": *[_type == 'news' && movie._ref == ^._id],
}),
...select(popularity > 20 && rating > 7.0 => {
"featured": true,
"awards": *[_type == 'award' && movie._ref == ^._id],
}),
}
// Specify sets of projections for different content types in an array
content[]{
_type == 'type1' => {
// Your selection of fields for type1
},
_type == 'type2' => {
// Your selection of fields for type2
"url": file.asset->url // Use joins to get data of referenced document
}
}
참조 조건부 처리 (Handling references conditionally)
배열 안에 참조와 비참조가 섞여 있으면, 각 항목의 _type을 보고 참조면 @->로 dereference하고, 아니면 @로 전체 객체를 반환하도록 조건을 줄 수 있어요.
'content': content[]{
_type == 'reference' => @->,
_type != 'reference' => @,
}
함수 (Functions)
GROQ는 참조 확인(references)·기본값(coalesce)·개수 세기(count)·반올림(round)·점수 계산(score, boost)·시맨틱 검색(text::semanticSimilarity) 같은 유용한 함수를 제공해요.
// any document that references the document
// with id person_sigourney-weaver,
// return only title
*[references("person_sigourney-weaver")]{title}
// Movies which reference ancient people
*[_type=="movie" && references(*[_type=="person" && age > 99]._id)]{title}
*[defined(tags)] // any document that has the attribute 'tags'
// coalesce takes a number of attribute references
// and returns the value of the first attribute
// that is non-null. In this example used to
// default back to the English language where a
// Finnish translation does not exist.
*{"title": coalesce(title.fi, title.en)}
// count counts the number of items in a collection
count(*[_type == 'movie' && rating == 'R']) // returns number of R-rated movies
*[_type == 'movie']{
title,
"actorCount": count(actors) // Counts the number of elements in the array actors
}
// round() rounds number to the nearest integer, or the given number of decimals
round(3.14) // 3
round(3.14, 1) // 3.1
// score() adds points to the score value depending
// on the use of the string "GROQ" in each post's description
// The value is then used to order the posts
*[_type == "post"]
| score(description match "GROQ")
| order(_score desc)
{ _score, title }
// boost() adds a defined boost integer to scores of items matching a condition
// Adds 1 to the score for each time $term is matched in the title field
// Adds 3 to the score if (movie > 3) is true
*[_type == "movie" && movieRating > 3] |
score(
title match $term,
boost(movieRating > 8, 3)
)
// Creates a scoring system where $term matching in the title
// is worth more than matching in the body
*[_type == "movie" && movieRating > 3] | score(
boost(title match $term, 4),
boost(body match $term, 1)
)
// Returns the body Portable Text data as plain text
*[_type == "post"]
{ "plaintextBody": pt::text(body) }
// text::semanticSimilarity() ranks results by semantic meaning
// Requires dataset embeddings to be enabled
// Only valid inside score()
*[_type == "product"]
| score(text::semanticSimilarity("leather waterproof boots"))
| order(_score desc)
{ _score, title }
// Hybrid search: combine keyword matching with semantic scoring
*[_type == "product"]
| score(
@ match text::query("waterproof boots"),
text::semanticSimilarity("waterproof boots")
)
| order(_score desc)
// Get all versions and drafts of a document. Use with the raw perspective or a perspective stack to ensure accurate results.
*[sanity::versionOf('document-id')]
// Get all documents that are part of a release. Use with the raw perspective to ensure accurate results.
*[sanity::partOfRelease('release-id')]
지리 정보 (Geolocation)
geo::distance·geo::contains·geo::intersects 같은 지리 함수로 위치 기반 필터링과 검색을 할 수 있어요.
// Returns all documents that are storefronts
// within 10 miles of the user-provided currentLocation parameter
*[
_type == 'storefront' &&
geo::distance(geoPoint, $currentLocation) < 16093.4
]
// For a given $currentLocation geopoint and deliveryZone area
// Return stores that deliver to a user's location
*[
_type == "storefront" &&
geo::contains(deliveryZone, $currentLocation)
]
// Creates a "marathonRoutes" array that contains
// all marathons whose routes intersect with the current neighborhood
*[_type == "neighborhood"] {
"marathonRoutes": *[_type == "marathon" &&
geo::intersects(^.neighborhoodRegion, routeLine)
]
}
산술 연산과 결합 (Arithmetic and concatenation)
표준 산술 연산을 지원하고, +는 문자열·배열·객체를 이어 붙일 수도 있어요. 다만 문자열과 숫자를 더할 때는 숫자를 먼저 문자열로 바꿔야 해요. 그렇지 않으면 null이 반환돼요.
// Standard arithmetic operations are supported
1 + 2 // 3 (addition)
3 - 2 // 1 (subtraction)
2 * 3 // 6 (multiplication)
8 / 4 // 2 (division)
2 ** 4 // 16 (exponentiation)
8 % 3 // 2 (modulo)
// Exponentiation can be used to take square- and cube-roots too
9 ** (1/2) // 3 (square root)
27 ** (1/3) // 3 (cube root)
// + can also concatenate strings, arrays, and objects:
"abc" + "def" // "abcdef"
[1,2] + [3,4] // [1,2,3,4]
{"a":1,"b":2} + {"c":3} // {"a":1,"b":2,"c":3}
// Concatenation of a string and a number requires the number be
// converted to a string. Otherwise, the operation returns null
3 + " p.m." // null
string(3) + " p.m." // "3 p.m."
더 알아보기 (Learn more)
- 공식 문서 — GROQ 쿼리 치트시트 원문