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).
Click signal becomes a ranking rule — with a self-revert safety net.
The Skryx engine watches what customers click on every search and detects (query, product) pairs that consistently win clicks but don't appear at the top. When a pair clears all safety gates, the engine creates a ranking rule automatically — and the circuit breaker monitors performance daily so a bad rule never sticks around.
Conservative by default.
A pair must clear EVERY one of these before the engine writes a rule:
- Plan flag —
auto_rerank: trueon tenant plan (Pro+ only) - Per-index opt-in — operator toggles "Auto-apply learned reranks" on the Ranking Rules page (default off)
- Signal quality — min click volume + share-of-clicks > 30% + CTR > 0.20 + high confidence rating
- Daily cap — max N rules per index per day (configurable per tenant, default 10) — prevents runaway storms
Pairs that already have a manual rule for the same (query, product) are skipped — the engine never fights the operator. A global kill switch in Filament admin can freeze the entire engine platform-wide for emergencies, no redeploy required.
// Auto-applied ranking rule { "id": 4218, "source": "auto_rerank", "index_id": 36, "name": "Auto rerank: 'supape' → BK52310", "type": "boost", "conditions": { "query_contains": ["supape"] }, "effect": { "filter_by": "id:=BK52310", "weight": 7 }, "priority": 60, "auto_applied_at": "2026-06-02T04:15Z", "baseline_ctr": 0.180 }
If a rule hurts, the engine takes it back.
RerankCircuitBreakerJob runs every day at 04:45. For each
auto-applied rule past the 3-day observation window, it compares the query's
current CTR to the baseline snapshotted at apply time. If the drop is > 10%,
the rule auto-reverts — disabled, timestamped with reverted_at,
and a Coach notification fires with the reason so the operator can review.
Rules that survive observation get marked validated. They keep
earning value silently. Long-running validated rules get a periodic re-check
so vocabulary drift can't quietly degrade them either.
- 3-day observation window per auto-applied rule
- 10% CTR drop threshold — configurable per tenant
- Coach notification on revert — operator stays in the loop
- Validated rules get re-checked every 30 days against fresh data
// 3 days later — observation closes // Healthy case: CTR held { "state": "validated", "baseline_ctr": 0.180, "current_ctr": 0.214, "delta_pct": "+18.9%", "validated_at": "2026-06-05T04:45Z" } // Unhealthy case: auto-revert { "state": "reverted", "baseline_ctr": 0.180, "current_ctr": 0.146, "delta_pct": "-18.9%", "reverted_reason": "CTR dropped 18.9%", "enabled": false }