Search

POST /1/indexes/:index/search — the request and response shape, and how to read explanations.

Text
POST /1/indexes/:index/search

Requires a search key, a scoped token, or a write/admin key.

Request

TypeScript
type SearchRequest = {
	q: string;
	mode?: 'typing' | 'committed';
	filters?: Filter[];
	facets?: string[];
	attributesToRetrieve?: string[];
	page?: number;
	hitsPerPage?: number;
	explain?: boolean;
	typoTolerance?: boolean;
	synonyms?: boolean;
	prefix?: boolean;
	minDataVersion?: number;
	clickAnalytics?: boolean;
};
FieldDefaultNotes
q—Required. Up to 512 characters.
mode'typing''typing' caps candidates at 200 for as-you-type speed; 'committed' (on Enter) raises the cap to 500.
filters[]See Filters below. At most 10 filters.
facets[]Attribute names to return counts for; must be in attributesForFaceting. At most 10.
attributesToRetrieveall attributesTop-level record attributes to return in each hit's record; objectID is always included. At most 50. See Choosing what a hit returns.
page0Zero-indexed.
hitsPerPage20Capped at 100.
explainfalseInclude the full ranking vector on every hit — see The ranking cascade.
typoToleranceindex settingOverride the index's default for this query only.
synonymsindex settingOverride the index's default for this query only.
prefixtrueSet false to require whole-word matches only.
minDataVersion—Wait for the primary to have applied at least this data version — see Read-your-writes.
clickAnalyticstrueInclude a queryID in the response for use with Click events. Set false to leave it out; queryID is then null.

Filters

TypeScript
type Filter =
	| { attribute: string; op: 'eq'; value: string | number | boolean }
	| { attribute: string; op: 'in'; values: (string | number)[] }
	| { attribute: string; op: 'range'; gte?: number; lte?: number };

Filters are always structured parameters, never a string you build yourself — there is no filter syntax to escape or get wrong. A request may carry at most 10 filters, and an in filter at most 20 values. A scoped token's own filters are applied on top of these.

JSON
{ "q": "highlights", "filters": [{ "attribute": "lang", "op": "in", "values": ["en", "zh-TW"] }] }

Request size

A search body may weigh at most 16,384 bytes. A larger body is refused with 413 and the request_too_large code, before it is parsed, and is not counted against your queries. A request over any other limit above is a 400 and is not counted either.

Choosing what a hit returns

By default each hit carries the whole stored record. Pass attributesToRetrieve to return only the attributes you render:

JSON
{ "q": "highlights", "attributesToRetrieve": ["title", "url"] }

This trims the response; it is not access control. Any caller with the key can leave the list out, or name any attribute, and highlight and snippet carry the text of every searchable attribute whatever the list says. Do not push private fields into records that a public search key can reach. Keep them in your own database, or put them in an index that only a scoped token can search.

Response

TypeScript
type SearchResponse = {
	query: string;
	correctedQuery: string | null;
	hits: Hit[];
	nbHits: number;
	page: number;
	hitsPerPage: number;
	facets: Record<string, Record<string, number>>;
	processingTimeMs: number;
	dataVersion: number;
	degraded: boolean;
	queryID: string | null;
};

Each hit:

TypeScript
type Hit = {
	objectID: string;
	record: Record<string, unknown>;
	highlight: Record<string, Highlighted>;
	snippet: Record<string, Highlighted>;
	explanation?: Explanation;
};

highlight and snippet mirror your searchable attributes; each is a list of segments marked matched or not, with which method found the match (word, bigram, typo or prefix) — enough to render CJK-safe highlighting without ever calling FTS highlight() yourself.

Example

Terminal
curl -X POST https://api.usesuo.com/1/indexes/docs/search \
  -H "x-suo-key: suo_search_YOUR_KEY" \
  -H "content-type: application/json" \
  -d '{ "q": "清华", "explain": true }'
JSON
{
	"query": "清华",
	"correctedQuery": null,
	"hits": [
		{
			"objectID": "campus-guide-tsinghua",
			"record": { "title": "清华大学校园指南" },
			"highlight": { "title": { "segments": [{ "text": "清华", "match": true, "via": "bigram" }, { "text": "大学校园指南", "match": false }], "fullyMatched": false } },
			"snippet": {},
			"explanation": { "vector": { "pinned": null, "words": 1, "typos": 0, "proximity": 0, "attribute": { "tier": 0, "position": 0, "name": "title" }, "exactness": 1, "custom": [], "bm25": 6.02 }, "decidedAgainstNextBy": "attribute", "matchedTerms": [{ "query": "清华", "indexed": "清华", "via": "bigram" }] }
		}
	],
	"nbHits": 1,
	"page": 0,
	"hitsPerPage": 20,
	"facets": {},
	"processingTimeMs": 6,
	"dataVersion": 481,
	"degraded": false,
	"queryID": null
}

Degraded results

If a dependency (typically the vector leg of a hybrid search past M8, or a replica under load) cannot answer in time, suo returns lexical-only results with degraded: true and the x-suo-degraded header, rather than failing the request. The widget shows a small notice and keeps the results on screen — see Degraded vs error.