Three effects. Three triggers. Stacked cleanly.
Boost, pin, or hide — composed via _eval arrays, run through a relevance-bucketing tie-breaker cascade, with a separate prefix-match Tier Sort guaranteeing "starts-with-the-query" results win.
// Tier 1: prefix-pinned IDs (titles starting with "sneakers") // Tier 2: _eval([ (in_stock:1):5, (brand:Anker):3 ]):desc, _text_match(buckets: 1):desc, points:desc, margin:desc
Two layers, composable.
Skryx separates rules (per-query if-this-then-that) from custom-ranking attributes (always-on tiebreakers). Rules express editorial decisions; custom-ranking attributes express your catalog's business priorities. They compose without fighting each other.
boost · pin · hide
Triggered by query patterns (exact / contains / starts-with) or by "every query". Stack on top of relevance — never replace it.
Ordered field cascade
Sortable fields (numeric, bool, or string with sort:true)
appended after a single relevance bucket. Cascade breaks ties left to
right.
Boost, pin, hide. No demote.
Penalising a product is just hiding it. The smaller surface keeps rules predictable and prevents the "two rules secretly cancelling each other" trap.
Add weight to anything matching a filter.
Multiple boosts compose into a single _eval([…]) array that
prepends to sort_by. Each boost is (filter):weight;
weights are positive integers. Matching documents get the weight summed
into their rank score before the text-match tiebreaker.
- Filter syntax:
field:value,field:>n,&&,|| - Safety validator rejects
field:(… && …)— a known Typesense footgun - Unsafe rules log a warning and are skipped, not crashing the query
{
"name": "Boost noise-cancelling",
"type": "boost",
"conditions": { "query_contains": "headphones" },
"effect": {
"filter_by": "tags:noise_cancelling",
"weight": 8
},
"priority": 10
}
Lock a specific document at a position.
A pin's effect carries document_id and position.
Apply per query (exact / contains / starts-with) or as an always-on
seasonal hold. Pinned hits push the rest of the result list down by one
slot — they don't replace organic results, they prepend.
- Per-rule
priorityorders multiple pins on the same query - Pinning is editorial: bypasses relevance entirely
- Visual Curator (a separate module) adds A/B testing + scheduling on top
{
"name": "Pin holiday hero",
"type": "pin",
"conditions": { "query": "sneakers" },
"effect": {
"document_id": "sku-air-jordan-1",
"position": 1
}
}
Drop a document or anything matching a filter.
Two shapes: hide a single SKU by document_id, or hide a
whole class via filter_by. Hides apply as engine-side
filters so the document never appears in results, facet counts, or
related-hits — even when semantic mode is on.
- Per-query hide (e.g., hide a competitor brand on a sponsored query)
- Always-on hide for OOS, region restrictions, age-gated SKUs
- Toggle
enabledon the rule — no redeploy, no engine restart
// Hide everything that's OOS { "type": "hide", "effect": { "filter_by": "in_stock:false" } } // Hide one product { "type": "hide", "effect": { "document_id": "sku-discontinued-42" } }
Three condition shapes. Or none — for always-on.
query
Exact, case-sensitive match. { "query": "sneakers" } fires
only when the literal query is "sneakers". Useful for editorial
decisions on canonical search terms.
query_contains
Substring match, case-insensitive. Value can be a string or an array.
{ "query_contains": ["headphone", "earbuds"] } fires when
the query contains either token.
query_starts_with
Prefix match, case-insensitive. Useful for category-style boosts.
{ "query_starts_with": "iphone" } fires for "iphone",
"iphone 15", "iphone case", etc.
Empty conditions = every query.
The most common ranking rule is "keep in-stock first" — applied to
every query. Just leave conditions empty (or omit). The
rule fires regardless of what the customer typed.
- Used by the six quick-start templates (in-stock boost, hide OOS, top-rated, …)
- Combines with query-scoped rules: always-on first, then query-specific stacks on top
{
"name": "In-stock first",
"type": "boost",
"conditions": null, // always-on
"effect": {
"filter_by": "in_stock:1",
"weight": 5
}
}
A tiebreaker cascade on sortable fields.
Separate from rules. You nominate an ordered list of fields and directions
in relevance_config.custom_ranking. Skryx appends them to
sort_by after a single-bucket text-match tier — so once
relevance has spoken, your business priorities decide the order.
Text match gets one tier. The rest is yours.
Skryx rewrites _text_match:desc as
_text_match(buckets: 1):desc when custom ranking is active.
All keyword-matched documents land in a single relevance tier, so your
custom fields absolutely dominate ordering inside that tier — the
Algolia-style "relevance, then custom ranking" cascade.
- Only sortable fields qualify (numeric, bool, or strings with
sort:true) - Cascade order respected: first tiebreaker decides, second only if first ties, etc.
- Mix
asc/descper field
// Index → Search Settings { "custom_ranking": [ { "field": "in_stock", "direction": "desc" }, { "field": "points", "direction": "desc" }, { "field": "margin", "direction": "desc" } ] }
"Actually starts with what I typed" wins.
Before the main search, Skryx runs a cheap prefix-only side query on the title head token (≥ 3 chars, diacritics folded). Documents whose title literally starts with the query become Tier 1 pinned hits, guaranteed top positions. Extra cost: 3–8 ms. Effect: zero "but why is the obvious match on page 2?" complaints.
- Pre-search side query, parallel-safe
- Both tiers share your sort_by — no special-cased rules
- Auto-disables when query is too short or contains operators
Six quick-start templates + raw editor.
The Ranking Rules page ships with six one-click templates so first-day tenants don't stare at a blank form: in-stock first, hide out-of-stock, discounted boost, top-rated boost, freshness boost, price-required hide. Power users get a raw JSON editor for conditions and effect under the Filament admin view.
in_stock:1 vs in_stock:true)source = auto_pilot · priority 30Per-index. Not per-tenant.
Every rule belongs to exactly one index. That's a deliberate constraint —
two indexes with the same name in different products will have different
ranking needs, and accidentally cross-applying a rule is a class of bug
Skryx prevents at the schema level. If you need the same rule on two
indexes, copy it explicitly (or use the copy-settings-from
lifecycle endpoint).