POST /v1/indexes/products/query 🔒 Bearer

Query an index

Run a search against an index — typo tolerance, synonyms, ranking and AI rewriting all included.

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 / false literally — never "true".
  • Strings in lists: no quotes around individual values inside […]. Write category:[Headphones,Speakers], not category:["Headphones","Speakers"].
  • AND wins for filters: {}: if you pass { brand: "Sony", color: "Black" }, Skryx translates to brand:Sony && color:Black. Use legacy filter_by if 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).
esc
POST /v1/indexes/products/query