The main search endpoint. Pass the query text, the fields to search in, and optionally filters, facets, sorting and paging. Skryx applies typo tolerance, synonyms and your ranking rules automatically — none of that needs extra flags.
If your plan has AI Query Understanding enabled and
the heuristic decides the query is worth enhancing, the rewritten query is
used internally and the original is returned in the ai_context field of the
response.
# Two input shapes
Skryx accepts two equivalent shapes for the same request — pick one and stick with it.
| Shape | When to use | Looks like |
|---|---|---|
| Skryx-native (recommended for new code) | Clean JSON objects, easier to build from app state | filters: { brand: ["Sony"] }, facets: ["brand"], sort: "price:asc" |
| Legacy string-form (back-compat with Typesense-style integrations) | Migration from Algolia / Typesense, copy-paste from older docs | filter_by: "brand:=Sony", facet_by: "brand", sort_by: "price:asc" |
Both reach the same code path. The native shape is translated to the legacy form internally before validation. Don't mix them in one request — if both keys are present, the legacy string wins.
# Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | required | The search query. Use * for browse mode. |
query_by |
string | optional | Comma-separated list of fields to search in. Defaults to the index's configured Searchable Attributes. |
query_by_weights |
string | optional | Per-field weights, same order as query_by (e.g. 5,3,1). Defaults to your index settings. |
search_mode |
string | optional, default auto |
One of auto, keyword, semantic, hybrid. See search modes. |
filters (native) |
object | optional | { field: value } or { field: [v1, v2] }. Multiple keys are AND'd. See Filtering. |
filter_by (legacy) |
string | optional | Raw filter expression string. See Filtering. |
facets (native) |
array | optional | ["brand", "category"]. Each field must be declared facet: true in the schema. |
facet_by (legacy) |
string | optional | Comma-separated list of facet fields. |
max_facet_values |
int | optional, default 10 | How many top values to return per facet. |
sort (native) |
string | optional | "field:asc" or "field:desc". |
sort_by (legacy) |
string | optional | Same shape as sort, multi-field allowed: "price:asc,rating:desc". |
per_page |
int | optional, default 10, max 250 | Page size. |
page |
int | optional, default 1 | Page number (1-indexed). |
prefix |
bool | optional, default true |
When true, the last query token is matched as a prefix. |
num_typos |
int | optional, 0–4 | Override the index's typo tolerance for this query. |
highlight_fields |
string | optional | Fields to wrap matched tokens in <mark>. |
fields |
array | optional | Array of field names to return per document (allowlist). id is always retained. When provided, exclude_fields is ignored. |
exclude_fields |
array | optional | Array of field names to strip from each document (blocklist). |
# Filtering
Filters are applied before scoring, so they don't slow ranked retrieval.
# Operator reference
| Operator | Example | Meaning |
|---|---|---|
: |
brand:Sony |
Match (uses the facet index for string fields). |
:= |
brand:=Sony |
Exact match — strict, recommended when you want unambiguous behaviour. |
:!= |
brand:!=Generic |
Negation / exclude. |
:> :< :>= :<= |
price:<500, rating:>=4 |
Numeric range. |
:[a..b] |
price:[100..500] |
Numeric range, inclusive. |
:[v1,v2,v3] |
category:[Headphones,Speakers] |
IN — match any value from the list. |
&& |
brand:=Sony && in_stock:=true |
AND. |
|| |
category:=Headphones || category:=Speakers |
OR. |
` ` (backticks) |
category:=`Wireless Headphones` |
Quote a value that contains spaces or special characters. |
A few rules that catch people:
- Backticks for spaces in the legacy form. The native form handles spaces automatically because each value is a separate JSON string — no quoting needed.
- Booleans: write
true/falseliterally — never"true". - Strings in lists: no quotes around individual values inside
[…]. Writecategory:[Headphones,Speakers], notcategory:["Headphones","Speakers"]. - AND wins for
filters: {}: if you pass{ brand: "Sony", color: "Black" }, Skryx translates tobrand:Sony && color:Black. Use legacyfilter_byif you need OR between fields.
# Native shape — one value per field
POST /v1/indexes/products/query
{
"q": "wireless headphones",
"filters": {
"brand": "Sony"
}
}
# Native shape — multiple values (OR within field, AND between fields)
POST /v1/indexes/products/query
{
"q": "wireless headphones",
"filters": {
"brand": ["Sony", "Bose"],
"in_stock": true
}
}
Internally becomes: brand:[Sony,Bose] && in_stock:true
# Native shape — numeric
{
"q": "*",
"filters": {
"category": "Headphones",
"price": 100
}
}
For ranges (price < 500, rating >= 4), use the legacy shape — the native
shape only expresses equality / IN.
# Legacy shape — full expressiveness
POST /v1/indexes/products/query
{
"q": "*",
"filter_by": "brand:=Sony && price:>=100 && price:<=500 && in_stock:=true"
}
# Legacy shape — values with spaces
{
"q": "*",
"filter_by": "category:=`Wireless Headphones` && brand:=`Apple Inc`"
}
# Legacy shape — OR across categories
{
"q": "*",
"filter_by": "category:=Headphones || category:=Earbuds || category:=Speakers"
}
# Combined with facets
Asking for facets returns aggregations next to your hits — useful for left-rail "refine results" UIs.
POST /v1/indexes/products/query
{
"q": "wireless headphones",
"query_by": "title,brand,category",
"filters": {
"brand": ["Sony", "Bose"]
},
"facets": ["brand", "category", "color"],
"per_page": 20
}
Response carries the facet aggregations at response.facets:
{
"hits": [ … ],
"found": 142,
"facets": {
"brand": { "Sony": 12, "Bose": 8, "JBL": 5 },
"category": { "Headphones": 18, "Earbuds": 7 },
"color": { "Black": 11, "White": 4, "Silver": 3 }
},
…
}
# Response
{
"hits": [
{
"document": { "id": "14", "title": "Sony WH-1000XM5", "brand": "Sony", "price": 379 },
"highlight": { "title": { "snippet": "<mark>Sony</mark> WH-1000XM5", "matched_tokens": ["Sony"] } },
"text_match": 578730054645712906,
"relevance_bonus":10,
"match_type": "keyword"
}
],
"found": 103,
"page": 1,
"per_page": 20,
"total_pages": 6,
"facets": { "brand": { "Sony": 12, "Bose": 8 } },
"search_time_ms": 8,
"search_mode": "keyword",
"ai_enhanced": false,
"ai_context": null,
"suggestions": [],
"related_hits": [],
"curated_applied": false,
"data": {
"_comment": "Legacy wrapper kept for back-compat — new code should read from the top-level fields above.",
"hits": [ … ],
"found": 103,
"out_of": 25964,
"facets": { … same dict as top-level facets … },
"facet_counts": [
{ "field_name": "brand", "counts": [ { "value": "Sony", "count": 12 } ] }
]
}
}
Read facets from response.facets (the dict). The response.data.facet_counts
array is the older Typesense-style shape, kept only so existing integrations
don't break.
# What's stripped by default
Skryx always strips internal fields from the response, regardless of any include / exclude params you pass:
embedding— the 512-dim vector used by semantic search. ~14 KB per hit; not consumable by any storefront code.- Anything starting with
_— internal engine annotations (_skryx_*,_text_match, etc.).
In other words: by default you get the catalogue fields you actually pushed,
nothing else. The fields and exclude_fields parameters narrow that
further — they can never add internal fields back. Asking for
fields: ["embedding"] returns just id (because internals are always
stripped, and id is always retained).
# Trimming the response further
Use fields when you know exactly which columns you need:
POST /v1/indexes/products/query
{ "q": "headphones", "fields": ["title", "price", "image_url"] }
Use exclude_fields when you want most fields but skip large ones:
{ "q": "headphones", "exclude_fields": ["description", "tags"] }
When both are present, fields wins.
# ai_context
When AI Query Understanding rewrites the query, this field is populated:
"ai_context": {
"original_query": "headphones for travel",
"rewritten_query": "noise cancelling wireless headphones",
"intent": "buy_part",
"user_message": "Looking for portable, noise-cancelling headphones for travel",
"alternative_queries": ["portable bluetooth headphones", "travel headphones"],
"confidence": "high"
}
It's safe to feed user_message directly to your UI. The original query is
preserved in original_query so you can render "Showing results for X — search
for original" UI if you want.
# Errors
| Status | Code | Reason |
|---|---|---|
| 400 | SK-SE-400 |
Missing q or query_by, or invalid filter_by expression. |
| 404 | SK-SE-404 |
The index does not exist. |
| 429 | SK-SYS-429 |
Plan quota exhausted or per-IP throttle hit. |
| 503 | SK-SE-503 |
Engine is unavailable (rare — see errors). |