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:
18@Recordable.DisplayName("Content API")9public class ContentApiEndpoint extends GCAEndpoint implements Singleton {101116@Override17protected GCASchemaSettings getSchemaSettings() {18return GCASchemaSettings.newBuilder()19.readonlyEntryClass(Article.class)20.readonlyEntryClass(Author.class)21.includeTypeSpecificEntryFields()22.defaultQueryLimit(20)23.maximumQueryLimit(100)24.allowGroupByQueries()25.build();26}27}
| Setting | Default | Description |
|---|---|---|
defaultQueryLimit(int) | 10 | Limit applied when the query doesn't specify one. |
maximumQueryLimit(int) | 200 | Largest limit a caller may request. Larger values produce an error. |
maximumQueryTimeout(Double) | none | Caps the options.timeout a caller may request. |
allowGroupByQueries() | off | Enables the groupBy argument. See Group-by queries. |
includeTypeSpecificEntryFields() | off | Adds strongly-typed per-type fields under Query/From. |
onlyAllowUniqueIndexLookups() | off | Removes 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 requiredfromargument selects what to query.Query/From/<Type>(...)— strongly-typed per-type fields, present whenincludeTypeSpecificEntryFields()is enabled.
Both forms accept the same arguments:
| Argument | Type | Java equivalent |
|---|---|---|
where | QueryPredicateInput | Query#where(String, Object...) |
having | QueryPredicateInput | Query#having(String, Object...) |
sortBy | [QuerySortInput!] | Query#sort* methods |
options | QueryOptionsInput | Query#noCache(), Query#master(), etc. |
offset | Long | Query#select(offset, limit) |
limit | Int | Query#select(offset, limit) |
groupBy | QueryGroupByInput | Query#groupByPartial(...) — only when enabled |
The result contains items (the records) and pageInfo (pagination metadata):
- GraphQL Query
- GraphQL Variables
- GraphQL Response
1query QueryArticles($where: QueryPredicateInput, $sortBy: [QuerySortInput!]) {2Query {3Records(from: {type: Article}, where: $where, sortBy: $sortBy, offset: 0, limit: 10) {4items {5__typename6_id7... on Article {8headline9tags10}11}12pageInfo {13count14hasNext15nextOffset16}17}18}19}20
1{2"where": {3// !tooltip[/predicate/] The same predicate language used by the Dari Query API. `?` placeholders are bound to the values in `arguments`.4"predicate": "headline matches ? && tags = ?",5// !tooltip[/arguments/] A list of values, one entry per `?` placeholder. An entry can itself be a list, which is how you express multi-value (IN style) comparisons.6"arguments": ["welcome", ["news", "sports"]]7},8"sortBy": [{9"field": {10"name": "headline",11"order": "ASCENDING"12}13}]14}15
1{2"data": {3"Query": {4"Records": {5"items": [6{7"__typename": "Article",8"_id": "0000019b-a37e-ddc0-af9f-b37edded0000",9"headline": "Welcome to Brightspot",10"tags": ["news", "welcome"]11}12],13"pageInfo": {14"count": 1,15"hasNext": false,16"nextOffset": 1017}18}19}20}21}22
The from argument
Query/Records requires a from argument—a one-of input mirroring the various Query.from* Java APIs:
| Field | Type | Description |
|---|---|---|
all | input | Query across all configured entry types (Query.fromAll()). Pass an empty object: {all: {}}. |
type | enum | A schema type name, e.g. {type: Article}. The enum values are visible in the GraphQL Explorer. |
typeId | UUID | The ObjectType ID. |
group | ID | An ObjectType group name (Query.fromGroup(...)). |
class | ID | A 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:
1input QueryPredicateInput {2predicate: String # predicate expression with ? placeholders3arguments: [[String!]!] # one entry per placeholder4namedValues: [...] # structured values referenced as $name5}
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
| Operator | Aliases | Description |
|---|---|---|
= | eq, equals, in, is | Equal to any of the given values. |
!= | ne, notequals, notin, <> | Not equal to all of the given values. |
<, <=, >, >= | lt, le, gt, ge | Numeric/lexicographic comparison. |
^= | startswith, sw | String starts with. |
contains | — | String contains. |
matches | matchesany, ~= | Full-text search match against any value. |
matchesall | — | Full-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;_anyperforms 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 likecms.content.publishDate.
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{2Query {3Records(4from: {type: Article}5where: {6predicate: "headline matches ?",7arguments: "$hw",8namedValues: [{9name: "hw",10value: {QueryPhrase: {phrase: "hello world", proximity: 2}}11}]12}13) {14items { _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:
| Sort | Fields | Java equivalent |
|---|---|---|
field | name, order: ASCENDING | DESCENDING | sortAscending / sortDescending |
distance | field, point: {latitude, longitude}, order: CLOSEST | FARTHEST | sortClosest / sortFarthest |
relevance | predicate (a QueryPredicateInput), weight | sortRelevant |
age | field, weight, order: NEWEST | OLDEST | sortNewest / 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:
1sortBy: [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:
| Field | Type | Description |
|---|---|---|
count | Long! | Total number of matching records. |
hasPrevious / hasNext | Boolean! | Whether earlier/later pages exist. |
offset / limit | Long! / Int! | The values used for this page. |
firstOffset / previousOffset / nextOffset / lastOffset | Long! | Offsets for navigation. |
firstItemIndex / lastItemIndex | Long! | 1-based index range of this page. |
pageIndex / pageCount / hasPages | — | Page-number style navigation. |
lastUpdate | Long | Most recent update timestamp among results, useful for cache validation. |
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:
- GraphQL Query
- GraphQL Response
1query ArticlesPerAuthor {2Query {3From {4Article(groupBy: {fields: ["author"]}, limit: 10) {5... on ArticleGroupBy {6groupings {7keys8count9itemsSelect(limit: 2) {10items {11... on Article {12headline13}14}15}16}17pageInfo {18count19}20}21}22}23}24}25
1{2"data": {3"Query": {4"From": {5"Article": {6"groupings": [7{8"keys": ["0000019b-aa01-d394-adbf-aa01280f0000"],9"count": 2,10"itemsSelect": {11"items": [12{"headline": "Welcome to Brightspot"},13{"headline": "A Second Story"}14]15}16}17],18"pageInfo": {19"count": 120}21}22}23}24}25}26
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"}.
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:
| Option | Description |
|---|---|
master | Read from the master database, bypassing replicas. |
noCache / noLatentCache | Bypass query caches. |
referenceOnly / resolveToReferenceOnly | Return unresolved references for performance. |
resolveInvisible | Include invisible (e.g. archived) records when resolving references. |
timeout | Per-query timeout in seconds, capped by maximumQueryTimeout. |
custom | Arbitrary {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
- Content Schema Types — what fields are available on result items
- Create, Update, and Delete Content — write operations
- Best Practices — query performance guidance