Skip to main content

Query Content Collections

The Query operation retrieves collections of content items with powerful filtering, sorting, and pagination capabilities that mirror Brightspot's Dari Query API. Unlike Get which fetches a single item by ID, Query enables you to search for content based on field values, sort results by any indexed field, and paginate through large datasets efficiently.

Query uses the same content type configuration as Get (via readonlyEntryClass) and returns the same content model response types. If you know the Java Query API, you already know this operation—every GraphQL argument maps directly to its Java counterpart: where to Query#where, sortBy to Query#sort, offset/limit to Query#select(offset, limit), and so on.

Configuration

Querying is available for every configured entry type. A few settings tune its behavior:

1
import java.util.Set;
2
3
import com.psddev.dari.db.Recordable;
4
import com.psddev.dari.db.Singleton;
5
import com.psddev.graphql.gca.GCAEndpoint;
6
import com.psddev.graphql.gca.GCASchemaSettings;
7
8
@Recordable.DisplayName("Content API")
9
public class ContentApiEndpoint extends GCAEndpoint implements Singleton {
10
11
@Override
12
public Set<String> getPaths() {
13
return Set.of("/content-api");
14
}
15
16
@Override
17
protected GCASchemaSettings getSchemaSettings() {
18
return GCASchemaSettings.newBuilder()
19
.readonlyEntryClass(Article.class)
20
.readonlyEntryClass(Author.class)
21
.includeTypeSpecificEntryFields()
22
.defaultQueryLimit(20)
23
.maximumQueryLimit(100)
24
.allowGroupByQueries()
25
.build();
26
}
27
}
SettingDefaultDescription
defaultQueryLimit(int)10Limit applied when the query doesn't specify one.
maximumQueryLimit(int)200Largest limit a caller may request. Larger values produce an error.
maximumQueryTimeout(Double)noneCaps the options.timeout a caller may request.
allowGroupByQueries()offEnables the groupBy argument. See Group-by queries.
includeTypeSpecificEntryFields()offAdds strongly-typed per-type fields under Query/From.
onlyAllowUniqueIndexLookups()offRemoves arbitrary querying entirely—only Get-style single-object fetches remain.

Anatomy of a query

Collection queries live under the root Query field:

  • Query/Records(from: ...) — the generic entry point. The required from argument selects what to query.
  • Query/From/<Type>(...) — strongly-typed per-type fields, present when includeTypeSpecificEntryFields() is enabled.

Both forms accept the same arguments:

ArgumentTypeJava equivalent
whereQueryPredicateInputQuery#where(String, Object...)
havingQueryPredicateInputQuery#having(String, Object...)
sortBy[QuerySortInput!]Query#sort* methods
optionsQueryOptionsInputQuery#noCache(), Query#master(), etc.
offsetLongQuery#select(offset, limit)
limitIntQuery#select(offset, limit)
groupByQueryGroupByInputQuery#groupByPartial(...) — only when enabled

The result contains items (the records) and pageInfo (pagination metadata):

1
query QueryArticles($where: QueryPredicateInput, $sortBy: [QuerySortInput!]) {
2
Query {
3
Records(from: {type: Article}, where: $where, sortBy: $sortBy, offset: 0, limit: 10) {
4
items {
5
__typename
6
_id
7
... on Article {
8
headline
9
tags
10
}
11
}
12
pageInfo {
13
count
14
hasNext
15
nextOffset
16
}
17
}
18
}
19
}
20

The from argument

Query/Records requires a from argument—a one-of input mirroring the various Query.from* Java APIs:

FieldTypeDescription
allinputQuery across all configured entry types (Query.fromAll()). Pass an empty object: {all: {}}.
typeenumA schema type name, e.g. {type: Article}. The enum values are visible in the GraphQL Explorer.
typeIdUUIDThe ObjectType ID.
groupIDAn ObjectType group name (Query.fromGroup(...)).
classIDA fully-qualified Java class name (Query.from(Class)).

Regardless of from, results are always restricted to the entry types configured in your schema settings—querying a type that isn't configured simply returns no results.

Filtering with where

The where argument accepts a predicate input with three fields:

1
input QueryPredicateInput {
2
predicate: String # predicate expression with ? placeholders
3
arguments: [[String!]!] # one entry per placeholder
4
namedValues: [...] # structured values referenced as $name
5
}

The predicate string uses the same predicate language as the Java API (Query#where("headline = ?", value)). Each ? placeholder is bound to the corresponding entry in arguments. An entry can itself be a list, which expresses multi-value comparisons—GraphQL conveniently also accepts bare values where lists are expected, so arguments: "welcome", arguments: ["welcome"], and arguments: [["welcome"]] are all equivalent.

Predicate operators

OperatorAliasesDescription
=eq, equals, in, isEqual to any of the given values.
!=ne, notequals, notin, <>Not equal to all of the given values.
<, <=, >, >=lt, le, gt, geNumeric/lexicographic comparison.
^=startswith, swString starts with.
containsString contains.
matchesmatchesany, ~=Full-text search match against any value.
matchesallFull-text search match against all values.

Predicates combine with && (and), || (or), and ! (not), with parentheses for grouping:

1
(headline matches ? || body matches ?) && tags = ? && !(author = missing)

A few special values and keys are available:

  • missing — matches records where the field has no value: author = missing.
  • _id, _type, _label, _any — metadata keys; _any performs a full-text match across all indexed fields.
  • Dot notation — traverse references and sub-fields: author/name = ? (slash) following Dari key syntax, including modification field prefixes like cms.content.publishDate.
note

Only indexed fields (@Indexed in Java) can be used in predicates and sorts. In the schema, indexed fields are marked with the @bsp_field(indexed: true) directive, so consumers can discover queryable fields directly from SDL or the GraphQL Explorer.

Structured predicate values

Some predicate values aren't expressible as strings—full-text phrases with proximity, or geospatial regions. Declare them in namedValues and reference them by $name in arguments:

1
{
2
Query {
3
Records(
4
from: {type: Article}
5
where: {
6
predicate: "headline matches ?",
7
arguments: "$hw",
8
namedValues: [{
9
name: "hw",
10
value: {QueryPhrase: {phrase: "hello world", proximity: 2}}
11
}]
12
}
13
) {
14
items { _id }
15
}
16
}
17
}

Available value types include QueryPhrase (phrase, proximity, weight), QueryWildcardPhrase, and Region (geospatial area), along with Missing.

Sorting

The sortBy argument takes a list of sorts, applied in order. Each entry is a one-of input:

SortFieldsJava equivalent
fieldname, order: ASCENDING | DESCENDINGsortAscending / sortDescending
distancefield, point: {latitude, longitude}, order: CLOSEST | FARTHESTsortClosest / sortFarthest
relevancepredicate (a QueryPredicateInput), weightsortRelevant
agefield, weight, order: NEWEST | OLDESTsortNewest / sortOldest

relevance and age are weighted sorts that combine: a common search pattern boosts items matching the search phrase in the headline AND published recently:

1
sortBy: [
2
{relevance: {predicate: {predicate: "headline matches ?", arguments: "brightspot"}, weight: 100}},
3
{age: {field: "cms.content.publishDate", weight: 100, order: NEWEST}}
4
]

Pagination

Results are paginated with offset and limit, and every result includes a pageInfo object mirroring Dari's PaginatedResult:

FieldTypeDescription
countLong!Total number of matching records.
hasPrevious / hasNextBoolean!Whether earlier/later pages exist.
offset / limitLong! / Int!The values used for this page.
firstOffset / previousOffset / nextOffset / lastOffsetLong!Offsets for navigation.
firstItemIndex / lastItemIndexLong!1-based index range of this page.
pageIndex / pageCount / hasPagesPage-number style navigation.
lastUpdateLongMost recent update timestamp among results, useful for cache validation.
Counting without fetching

If you select only pageInfo { count } and omit items, the GCA optimizes the operation to a Query#count() call—no records are fetched at all.

Group-by queries

With allowGroupByQueries() enabled, the groupBy argument groups results by one or more indexed fields, mirroring Query#groupByPartial. Enabling this changes the entry field's return type to a union of the select result and the group-by result, so queries must select via ... on fragments:

1
query ArticlesPerAuthor {
2
Query {
3
From {
4
Article(groupBy: {fields: ["author"]}, limit: 10) {
5
... on ArticleGroupBy {
6
groupings {
7
keys
8
count
9
itemsSelect(limit: 2) {
10
items {
11
... on Article {
12
headline
13
}
14
}
15
}
16
}
17
pageInfo {
18
count
19
}
20
}
21
}
22
}
23
}
24
}
25

Each grouping exposes:

  • keys — the grouped field values for this group (records appear as their UUID).
  • count — the number of records in the group.
  • min(field), max(field), sum(field), nonNullCount(field) — aggregates computed per group.
  • itemsSelect(offset, limit) — the records belonging to the group, with nested pagination.

Use the having argument to filter groups by aggregate values, e.g. authors with more than five articles: having: {predicate: "_count > ?", arguments: "5"}.

warning

Because enabling group-by changes the return type of every query entry field from a type to a union, turning it on (or off) is a breaking schema change for existing consumers. Decide early.

Query options

The options argument exposes Dari's query modifiers:

OptionDescription
masterRead from the master database, bypassing replicas.
noCache / noLatentCacheBypass query caches.
referenceOnly / resolveToReferenceOnlyReturn unresolved references for performance.
resolveInvisibleInclude invisible (e.g. archived) records when resolving references.
timeoutPer-query timeout in seconds, capped by maximumQueryTimeout.
customArbitrary {key, value} pairs passed to Query#option(...).

Visibility-aware filtering

Queries without a concrete type (from: {all: {}}) cannot always apply visibility rules (like archived status) as database predicates. If you see invisible records leaking into generic queries, filterVisibilityUnawareQueriesInMemory() filters them in memory instead—at the cost of pagination accuracy (pageInfo.hasNext remains reliable; count may not be). Reserve it for small datasets.

Next steps

Was this page helpful?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.