Search
POST /1/indexes/:index/search — the request and response shape, and how to read explanations.
POST /1/indexes/:index/searchRequires a search key, a scoped token, or a write/admin key.
Request
type SearchRequest = {
q: string;
mode?: 'typing' | 'committed';
filters?: Filter[];
facets?: string[];
page?: number;
hitsPerPage?: number;
explain?: boolean;
typoTolerance?: boolean;
synonyms?: boolean;
prefix?: boolean;
minDataVersion?: number;
clickAnalytics?: boolean;
};| Field | Default | Notes |
|---|---|---|
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. |
facets | [] | Attribute names to return counts for; must be in attributesForFaceting. |
page | 0 | Zero-indexed. |
hitsPerPage | 20 | Capped at 100. |
explain | false | Include the full ranking vector on every hit — see The ranking cascade. |
typoTolerance | index setting | Override the index's default for this query only. |
synonyms | index setting | Override the index's default for this query only. |
prefix | true | Set false to require whole-word matches only. |
minDataVersion | — | Wait for the primary to have applied at least this data version — see Read-your-writes. |
clickAnalytics | false | Include a queryID in the response for use with Click events. |
Filters
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.
{ "q": "highlights", "filters": [{ "attribute": "lang", "op": "in", "values": ["en", "zh-TW"] }] }Response
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:
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
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 }'{
"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.