Search — Overview
The HappyColis API provides a unified, Elasticsearch-backed search system shared across all major domains: orders, products, stock, shipments, and delivery orders. Every searchable entity exposes the same input types, so the patterns you learn in one domain apply everywhere.
How Search Works
Under the hood, every domain index service syncs data from PostgreSQL into Elasticsearch. When you call a search query, the API translates the structured GraphQL input into an Elasticsearch query, executes it, and returns paginated results along with optional facet aggregations.
Because the search layer is Elasticsearch, you benefit from:
- Sub-second response times even over millions of records
- Full boolean filter logic (AND / OR / NOT)
- Facet aggregations (bucket counts per field value)
- Nested document filtering (filter on arrays of objects, e.g. order lines)
- Cursor-based pagination for unbounded result sets
Pagination Modes
Two input types are available depending on how many results you expect:
| Mode | Input Type | Use When | Max Results |
|---|---|---|---|
| Offset-based | SearchInput | < 10,000 results; supports random page access | 10,000 |
| Cursor-based | CursorSearchInput | > 10,000 results; sequential access only | Unlimited |
SearchInput
Use SearchInput for everyday paginated queries. Supports a zero-based page number and hitsPerPage to control the result window.
input SearchInput {
query: String # Full-text search query (Elasticsearch query_string syntax)
fields: [String] # Fields to search in (default: all text fields)
complexFilters: ComplexFilterInput # Structured boolean filters (recommended)
facets: [String] # Facet/aggregation names, or ["*"] for all
sort: [SortInput] # Sort instructions
hitsPerPage: Int # Results per page (default: 50)
page: Int # Zero-based page number (default: 0)
# Deprecated — use complexFilters instead
filters: JSONObject
facetFilters: JSONObject
numericFilters: [NumericFilterInput]
}CursorSearchInput
Use CursorSearchInput when you need to page through more than 10,000 records (exports, full syncs, etc.). Replace page with a cursor field received from the previous response.
input CursorSearchInput {
query: String
fields: [String]
complexFilters: ComplexFilterInput
facets: [String]
sort: [SortInput]
hitsPerPage: Int
cursor: String # Base64 cursor from previous response's nextCursor
}Cursor pagination rules:
- First request: omit
cursor. - The response includes
nextCursor(Base64 string) andhasNextPage(boolean). - Pass
nextCursorascursorin the next request. - Stop when
hasNextPageisfalseornextCursorisnull.
Cursor-based pagination does not support jumping to arbitrary pages. Use it for sequential full-dataset processing only.
SortInput
input SortInput {
property: String! # Elasticsearch field name
order: OrderBy! # asc | desc
}Multiple sort fields are applied in order. For cursor-based pagination, a tiebreaker field (objectID) is automatically appended to ensure consistent ordering across pages.
Example — sort by issue date descending, then by ID:
sort: [
{ property: "issuedAt", order: desc },
{ property: "objectID", order: asc }
]Facets / Aggregations
Passing facet names alongside a search request returns bucket counts for those fields in the response. This is useful for building filter UIs.
# Request specific facets
facets: ["state", "type"]
# Request all available facets for the domain
facets: ["*"]Response shape:
{
"facets": {
"state": {
"OPENED": 142,
"COMPLETED": 87,
"DRAFT": 23
},
"type": {
"B2C": 198,
"B2B": 54
}
}
}Response Shape
All search queries return a consistent response structure:
type OrderSearchResult {
hits: [OrderRecord!]! # The matched records for the current page
nbHits: Int! # Total number of matching records
nbPages: Int # Total number of pages (offset-based only)
page: Int # Current page number (offset-based only)
nextCursor: String # Cursor for the next page (cursor-based only)
hasNextPage: Boolean # Whether more pages exist (cursor-based only)
facets: JSONObject # Aggregation buckets (when facets requested)
}Domain Examples
Orders
query SearchOrders($input: SearchInput!) {
orders(search: $input) {
nodes {
id
state
externalReference
total
}
total
pageInfo {
hasNextPage
endCursor
}
facets {
field
buckets {
value
count
}
}
}
}Variables:
{
"input": {
"query": "*",
"complexFilters": {
"must": [
{ "property": "organizationId", "operator": "in", "values": ["org-abc123"] },
{ "property": "state", "operator": "in", "values": ["OPENED"] }
]
},
"sort": [{ "property": "issuedAt", "order": "desc" }],
"hitsPerPage": 25,
"page": 0
}
}Products
query SearchProducts($input: SearchInput!) {
products(search: $input) {
nodes {
id
title
sku
status
}
total
facets {
field
buckets {
value
count
}
}
}
}Variables:
{
"input": {
"query": "running shoes",
"complexFilters": {
"must": [
{ "property": "organizationId", "operator": "in", "values": ["org-abc123"] },
{ "property": "active", "operator": "in", "values": ["true"] }
]
},
"facets": ["brand", "category"],
"sort": [{ "property": "createdAt", "order": "desc" }],
"hitsPerPage": 24,
"page": 0
}
}Stock References
query SearchStock($input: SearchInput!) {
stockReferences(search: $input) {
nodes {
id
sku
availableQuantity
locationId
}
total
}
}Variables:
{
"input": {
"complexFilters": {
"must": [
{ "property": "organizationId", "operator": "in", "values": ["org-abc123"] },
{ "property": "locationId", "operator": "in", "values": ["loc-warehouse-1"] },
{ "property": "availableQuantity", "operator": "gt", "value": 0 }
]
},
"sort": [{ "property": "availableQuantity", "order": "asc" }],
"hitsPerPage": 100,
"page": 0
}
}Further Reading
- Complex Filters — Boolean logic with
must,mustNot, andshould - Facet Filters — Filtering on categorical/keyword fields
- Numeric Filters — Filtering on numeric and date fields
- Nested Filters — Filtering on nested document fields (arrays of objects)