Skip to main content

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:

ParameterTypeRequiredDescription
qstringNoSearch 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 }:

FieldTypeDescription
detectivestringThe search-agent id. Always "local".
idstringSynthetic item id, "<detective>:<index>" (e.g. "local:0").
valueobjectThe matched hit (see below).

The hit lives in value:

FieldTypeDescription
typestringThe builder id: dataset, report, template, adhoc, integration, app, user, team, toolkit, and others.
labelstringDisplay label for the hit.
infostringSecondary text. Often an empty string.
descriptionstringThe entity's description.
dataobjectIdentifiers for the hit (id, naturalId, and sometimes type).
filterGroupstringThe UI bucket the builder assigns. Datasets land in dataset; Reports, Templates, and queries land in report.
imagestringOptional 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.

Search across content typesGET /api/search?q=Order Desk
GET /api/search?q=... runs the `local` search agent, which UNION ALLs a parameterized `ilike %q%` query over every read-access-filtered search:local builder (datasets, reports, templates, queries, integrations, apps, users, teams, toolkits, ...). The body is a flat paged Collection `{ items, start, count, total }` — NOT the `_embedded.inf:datasets.results` shape the prose shows. Each item is `{ detective: "local", id: "local:<n>", value }`, and the hit lives in `value`: `{ type, label, info, description, data, filterGroup, image? }`. `type` is the builder id (dataset, report, ...), `data` is a JSON object of identifiers, and `filterGroup` is the UI bucket (datasets -> "dataset", reports/templates/queries -> "report"). Matching is case-insensitive substring.
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.
Stable ordering

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.

Search with no queryGET /api/search
GET /api/search with no `q` returns an EMPTY Collection (`total: 0`, `items: []`), NOT discovery information about the available search drivers as the prose claims. The single registered search agent (`local`) is eligible only when `q` is defined, so with no query nothing runs and no driver list is emitted.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/search"
}
},
"items": [],
"start": 0,
"count": 0,
"total": 0
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

SQL injection is escapedGET /api/search?q=North' OR 'A'='A
The query is fully parameterized (a `:search` replacement bound to `%q%`), so SQL-injection attempts are matched literally rather than executed (I5-9602). This `North' OR 'A'='A` probe matches no row and returns `total: 0`.
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.
Substring, not full text

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.