Home· Features· Custom Ranking
🎯 Custom Ranking

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.

Starter Growth Scale Enterprise
Composed sort_by for "sneakers"
// 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
The model

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.

Rules · per query

boost · pin · hide

Triggered by query patterns (exact / contains / starts-with) or by "every query". Stack on top of relevance — never replace it.

Custom ranking · always-on

Ordered field cascade

Sortable fields (numeric, bool, or string with sort:true) appended after a single relevance bucket. Cascade breaks ties left to right.

The three rule effects

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.

📈 boost

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
}
📌 pin

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 priority orders 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
  }
}
🚫 hide

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 enabled on 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" }
}
Rule triggers

Three condition shapes. Or none — for always-on.

1

query

Exact, case-sensitive match. { "query": "sneakers" } fires only when the literal query is "sneakers". Useful for editorial decisions on canonical search terms.

2

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.

3

query_starts_with

Prefix match, case-insensitive. Useful for category-style boosts. { "query_starts_with": "iphone" } fires for "iphone", "iphone 15", "iphone case", etc.

Always-on rules

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
  }
}
Custom-ranking attributes

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.

Bucketed relevance

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 / desc per field
// Index → Search Settings
{
  "custom_ranking": [
    { "field": "in_stock", "direction": "desc" },
    { "field": "points",   "direction": "desc" },
    { "field": "margin",   "direction": "desc" }
  ]
}
Tier Sort prefix pinning

"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
Query: "sneakers"
─────────────────
Tier 1 (prefix pinned)
1. Sneakers Air Jordan 1
2. Sneakers Nike Pegasus
Tier 2 (relevance + rules)
3. Adidas Stan Smith sneakers
4. Puma Suede sneakers
In the dashboard

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.

6
Quick-start templates · adapt to your schema (e.g., in_stock:1 vs in_stock:true)
Auto-Pilot
First-batch sync suggests rules with source = auto_pilot · priority 30
~100 ms
From "Apply" click in the dashboard to a live rule in the engine
Scoping

Per-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).

Keep exploring

Other things Skryx does

Try it on your own catalog.

Free tier, no credit card. EU-hosted from day one.