Searching
One filter language across all searchable record types.
Overview
Every searchable record type in ShipGenius is reachable through one namespace, searches.
All entity types — orders, items, shipments, inventory, users, blueprints and the rest —
expose the same three operations and take the same filter language.
query {
searches {
order {
by_id(entity_id: "a58f2d04-6b39-4c17-9e85-1d7b30f2a9c6") {
id
}
search(search: { limit: 25 }) {
rows {
id
}
}
aggregate(search: []) {
count
}
}
}
}
| Operation | Returns | Use for |
|---|---|---|
by_id | The entity | Fetching one known record |
search | A paginated result | Listing, filtering, sorting |
aggregate | SearchAggregations | Counts and maths without pulling rows |
The search query
search takes a single
PaginatedSearchQuery:
| Field | Type | Default | Purpose |
|---|---|---|---|
limit | Int! | 5 | Number of rows per page. |
cursor | String | null | Where to resume from |
reverse | Boolean! | false | Walk the result set backwards |
filter | [FilterExpression!] | null | Which rows |
order_by | [SearchOrder!] | null | What order |
The result carries the rows and the cursors. These are the fields available on the
selection set of search:
search(search: $search) {
rows {
id
}
has_next_page
has_previous_page
next_cursor
previous_cursor
}
Filter expressions
A filter is a tree. Each node is a FilterExpression with exactly one of:
| Field | Meaning |
|---|---|
expression | A single comparison |
and | All child expressions must match |
or | Any child expression must match |
not | The child expression must not match |
filter_string | A raw filter string |
A top-level filter is a list, and the entries are combined with AND.
A single comparison
FieldFilter is symmetrical —
left and right are both
ExpressionField, so you can compare a
column to a literal, a column to another column, or a column to an aggregate.
{
"expression": {
"left": { "column": "status" },
"op": "=",
"right": { "literal": { "value": "READY_TO_SHIP" } }
}
}
An ExpressionField is one of four things:
| Form | Example | Meaning |
|---|---|---|
column | { "column": "status" } | A field on the entity |
literal | { "literal": { "value": 5 } } | A constant |
relative_time | { "relative_time": { "days": -7 } } | An offset from now |
aggregation | see below | A value computed from a related table |
Combining
Orders that are ready to ship and were created in the last week:
{
"search": {
"limit": 100,
"filter": [
{
"and": [
{
"expression": {
"left": { "column": "status" },
"op": "=",
"right": { "literal": { "value": "READY_TO_SHIP" } }
}
},
{
"expression": {
"left": { "column": "created_at" },
"op": ">=",
"right": { "relative_time": { "days": -7 } }
}
}
]
}
]
}
}
Relative time
ExpressionRelativeTime takes
years, months, weeks, days, hours, minutes and seconds, all defaulting to
0. Negative values are in the past.
{ "relative_time": { "days": -30 } }
This is evaluated server-side at query time, so a saved filter stays correct instead of freezing to the date you wrote it.
Aggregations inside a filter
ExpressionAggregation computes a value from a related table and compares against it — "orders with more than three lines" without fetching any lines:
{
"expression": {
"left": {
"aggregation": {
"ref_table": "OrderLine",
"ref_key": "order",
"key": "id",
"agg_method": "COUNT",
"agg_column": "id"
}
},
"op": ">",
"right": { "literal": { "value": 3 } }
}
}
| Field | Default | Meaning |
|---|---|---|
ref_table | — | The graph type to aggregate over, e.g. OrderLine |
ref_key | — | The field on ref_table referencing this entity, e.g. order |
key | id | The field on this entity that ref_key points at |
agg_method | — | See AggregationMethod |
agg_column | id | The field being aggregated |
with_filter | null | Narrows the rows the aggregate is computed over |
with_filter takes a full filter expression, so you can count only some of the related
rows — for example, only unfulfilled lines.
Which columns can I filter on?
Fields carry a @filterable marker in the schema, and filterable fields also record what
they join to. The entity's type page in this reference lists the field names to use. The
type a filterable field joins to is what you name as the ref_table of an aggregation —
the graph type, such as OrderLine, not a database table name.
Sorting
SearchOrder is a column plus a direction.
order_by is a list, applied in order:
{
"order_by": [
{ "column": "created_at", "reverse": true },
{ "column": "external_order_id", "reverse": false }
]
}
NOTE
Sort on a unique column, or include one as a tiebreaker. Cursor pagination over a non-deterministic order can repeat or skip rows between pages.
Pagination
Cursor-based, not offset-based. Send the previous response's next_cursor back as
cursor.
query PageOrders($search: PaginatedSearchQuery) {
searches {
order {
search(search: $search) {
rows {
id
external_order_id
}
has_next_page
next_cursor
}
}
}
}
A complete walk:
async function* allOrders(client, filter) {
let cursor = null;
for (;;) {
const { data } = await client.query({
query: PAGE_ORDERS,
variables: { search: { limit: 200, cursor, filter } },
});
const page = data.searches.order.search;
yield* page.rows;
if (!page.has_next_page) {
return;
}
cursor = page.next_cursor;
}
}
WARNING
Stop on has_next_page, not on an empty rows array. Keep the filter identical across
pages — changing it mid-walk invalidates the cursor.
Aggregating without fetching rows
aggregate takes the same filter list and returns
SearchAggregations. Note that it takes
[FilterExpression!] directly, not a PaginatedSearchQuery.
query OrderStats($search: [FilterExpression!]) {
searches {
order {
aggregate(search: $search) {
count
sum(column: "total_price")
average(column: "total_price")
min(column: "created_at")
max(column: "created_at")
}
}
}
}
sum, average, min, max and standard_deviation each take a column and return
JSON, so the value comes back typed as the column is.
Saved filters
A filter expression can be stored and reused via EntityFilterInput:
{
"filter_expression_entity_type": "ORDER",
"name": "Ready to ship, last 7 days",
"filter_expression": [
{ "expression": { "left": { "column": "status" }, "op": "=", "right": { "literal": { "value": "READY_TO_SHIP" } } } }
],
"allow_full_access": false
}
Because relative times are evaluated at query time, a saved filter keeps meaning "the last seven days" rather than the specific week it was created in.
Worked example
Every order for one marketplace integration, created in the last 30 days, with more than three lines, newest first, fetched 200 at a time:
query ProblemOrders($search: PaginatedSearchQuery) {
searches {
order {
search(search: $search) {
rows {
id
external_order_id
created_at
}
has_next_page
next_cursor
}
}
}
}
{
"search": {
"limit": 200,
"order_by": [
{ "column": "created_at", "reverse": true },
{ "column": "id", "reverse": true }
],
"filter": [
{
"and": [
{
"expression": {
"left": { "column": "marketplace_integration_id" },
"op": "=",
"right": { "literal": { "value": "e6b41f83-0d57-49a2-b31c-8f52a06d7e94" } }
}
},
{
"expression": {
"left": { "column": "created_at" },
"op": ">=",
"right": { "relative_time": { "days": -30 } }
}
},
{
"expression": {
"left": {
"aggregation": {
"ref_table": "OrderLine",
"ref_key": "order",
"agg_method": "COUNT"
}
},
"op": ">",
"right": { "literal": { "value": 3 } }
}
}
]
}
]
}
}
Searches and exports
Data exports cover the same records asynchronously and return a file, rather than holding a cursor open across many requests. See Choosing between export and search.
Next
Data exports →