Search API
The Search API exposes a single endpoint, GET /api/search, that runs a unified
substring search across the tenant's content (Datasets, Reports, Templates,
queries, Integrations, Apps, Users, Teams, toolkits, and more).
The route has no permission pre-block. It is a plain authenticated GET, and its
only validation is q: joi.string().empty(''), so q is optional and an empty
string is coerced to undefined. Results are read-access filtered, so each caller
sees only the rows they are allowed to read.
GET /api/search
Search across every content type the tenant exposes.
Authentication: Required (session or API token). No special permission.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | No | Search term. An empty string is treated as no query. |
Response:
A flat paged Collection envelope { items, start, count, total }. There is no
per-content-type grouping in the payload (no _embedded.inf:datasets.results
shape). Each item is { detective, id, value }:
| Field | Type | Description |
|---|---|---|
detective | string | The search-agent id. Always "local". |
id | string | Synthetic item id, "<detective>:<index>" (e.g. "local:0"). |
value | object | The matched hit (see below). |
The hit lives in value:
| Field | Type | Description |
|---|---|---|
type | string | The builder id: dataset, report, template, adhoc, integration, app, user, team, toolkit, and others. |
label | string | Display label for the hit. |
info | string | Secondary text. Often an empty string. |
description | string | The entity's description. |
data | object | Identifiers for the hit (id, naturalId, and sometimes type). |
filterGroup | string | The UI bucket the builder assigns. Datasets land in dataset; Reports, Templates, and queries land in report. |
image | string | Optional icon URL added by some builders (e.g. a dashboard Report's icon). |
When q is supplied, the local search agent runs one parameterized ilike %q%
query (UNION ALL) over every eligible search:local builder, each read-access
filtered and limited to 100 rows per builder, and returns the matching hits.
Matching is case-insensitive substring matching.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/search?q=Order%20Desk"
}
},
"items": [
{
"detective": "local",
"id": "local:0",
"value": {
"type": "dataset",
"label": "Order Desk",
"info": "",
"description": "Open orders awaiting fulfillment.",
"data": {
"id": "00000000-0000-4000-8000-000000000001",
"naturalId": "admin:order-desk"
},
"filterGroup": "dataset"
}
},
{
"detective": "local",
"id": "local:1",
"value": {
"type": "report",
"label": "Order Desk Dashboard",
"info": "",
"description": "Live view of the order desk queue.",
"data": {
"id": "00000000-0000-4000-8000-000000000002",
"naturalId": "admin:order-desk-dashboard",
"type": "dashboard"
},
"image": "/assets/reports/images/dashboard.svg",
"filterGroup": "report"
}
}
],
"start": 0,
"count": 2,
"total": 2
}
The hits come from a UNION ALL with no global ORDER BY, so the order of items is
not guaranteed run to run. Sort the items client-side if you need a deterministic
order.
No query
GET /api/search with no q (or an empty q) returns an empty Collection
(total: 0, items: []). The single registered search agent (local) is
eligible only when q is defined, so with no query nothing runs. The endpoint
does not return discovery information about the available search drivers, and it
does not enumerate the drivers in the payload.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/search"
}
},
"items": [],
"start": 0,
"count": 0,
"total": 0
}
Query escaping
The search term is fully parameterized: a :search replacement bound to %q%.
SQL-injection attempts are therefore matched literally rather than executed
(I5-9602). A probe such as North' OR 'A'='A matches no row and returns
total: 0.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/search?q=North%27%20OR%20%27A%27%3D%27A"
}
},
"items": [],
"start": 0,
"count": 0,
"total": 0
}
Search is a case-insensitive %q% substring match over raw tables, not a
full-text or relevance-ranked search. There is no Elasticsearch indexing step,
and there is no global relevance ranking across content types.