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
            }
        }
    }
}
OperationReturnsUse for
by_idThe entityFetching one known record
searchA paginated resultListing, filtering, sorting
aggregateSearchAggregationsCounts and maths without pulling rows

The search query

search takes a single PaginatedSearchQuery:

FieldTypeDefaultPurpose
limitInt!5Number of rows per page.
cursorStringnullWhere to resume from
reverseBoolean!falseWalk the result set backwards
filter[FilterExpression!]nullWhich rows
order_by[SearchOrder!]nullWhat 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:

FieldMeaning
expressionA single comparison
andAll child expressions must match
orAny child expression must match
notThe child expression must not match
filter_stringA 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:

FormExampleMeaning
column{ "column": "status" }A field on the entity
literal{ "literal": { "value": 5 } }A constant
relative_time{ "relative_time": { "days": -7 } }An offset from now
aggregationsee belowA 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 } }
    }
}
FieldDefaultMeaning
ref_tableThe graph type to aggregate over, e.g. OrderLine
ref_keyThe field on ref_table referencing this entity, e.g. order
keyidThe field on this entity that ref_key points at
agg_methodSee AggregationMethod
agg_columnidThe field being aggregated
with_filternullNarrows 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.