From Algolia

Migrate from Algolia to Skryx in under an hour, including an interactive config converter.

If you're coming from Algolia, the move to Skryx is intentionally cheap. The query shape is similar, the concepts overlap almost 1:1, and the configuration translates directly — most teams cut over in an afternoon.

# Concept mapping

Algolia Skryx
Application Tenant
Index Index
Searchable attributes query_by + per-index search_attributes
customRanking Ranking rules (boost / pin / hide)
attributesForFaceting Schema fields with facet: true
Synonyms Synonyms (direct mapping)
Replicas (sort variants) sort_by parameter (no replica needed)
Query Suggestions /v1/indexes/{name}/suggest

# Step-by-step

# 1. Provision a Skryx index

Same shape as your Algolia index, declared as a schema. The create-index reference has the full body.

# 2. Bulk-load your documents

If you can export your Algolia records as JSON, use Skryx's batch endpoint — 1,000 docs per request. For a million-document index, expect 2–3 minutes with parallel batches.

If your source is a feed URL (Google Merchant XML, Meta Catalog), skip the manual push and use a data source — Skryx pulls on a schedule.

# 3. Convert your settings

Use the migration helper below. Paste your Algolia getSettings() JSON and you'll get the Skryx equivalent side-by-side, with notes on anything that needs manual review.

# 4. Switch the endpoint

Most clients call Algolia like this:

const { hits } = await index.search('headphones', { hitsPerPage: 20 });

Skryx is a plain fetch:

const { data } = await fetch('https://api.skryx.io/v1/indexes/products/query', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer ' + SKRYX_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: 'headphones', query_by: 'title,brand,category', per_page: 20 })
}).then(r => r.json());
// data.hits is your replacement for hits

Field-by-field differences:

Algolia Skryx
hits[].objectID data.hits[].document.id
hits[]._highlightResult data.hits[].highlight
nbHits data.found
processingTimeMS data.search_time_ms
query (echoed back in data.ai_context.original_query only when AI fired)

# 5. Wire facets and filters

facetFilters becomes filter_by with && / ||:

Algolia: { facetFilters: [["brand:Sony"], ["price:<500"]] }
Skryx:   filter_by: "brand:Sony && price:<500"

# Requesting facets

Facets are opt-in in Skryx. Mark the fields with facet: true in the index schema (Search Settings → Schema), then ask for them in the request body:

// Skryx-native shape (recommended)
{
  "q": "tractor",
  "query_by": "title",
  "facets": ["category", "subcategory", "brand"],   // ← array of field names
  "page": 1,
  "per_page": 20
}

If a field isn't marked facet: true in the schema, asking for it has no effect (no error, just no aggregation back).

# Response shape — Skryx ↔ Algolia

This is the part most migrations get wrong. Skryx returns facets in a shape that's already identical to Algolia's, but our response also carries a legacy data.* wrapper for backwards-compat with older integrations. Read from the top level, not from data.*.

// Full response (trimmed)
{
  "hits":          [ … ],
  "found":         1550,
  "page":          1,
  "per_page":      20,
  "total_pages":   78,
  "facets": {                                 // ← read from HERE
    "category": {
      "Piese pentru Tractor Forestier": 229,
      "Piese tractoare straine":         64,
      "Piese utilaje agricole":          61
    },
    "subcategory": { … },
    "brand":       { "UTB": 1550 }
  },
  "search_time_ms": 18,
  "search_mode":    "keyword",
  "ai_enhanced":    false,
  "ai_context":     { … },
  "suggestions":    [ … ],
  "related_hits":   [ … ],
  "curated_applied": false,

  // Legacy wrapper — only here for back-compat with old integrations.
  // New code should NOT read from this.
  "data": {
    "facets":       { …same dict… },         // duplicate
    "facet_counts": [                         // ← Typesense legacy ARRAY
      { "field_name": "category",
        "counts": [
          { "value": "Piese pentru Tractor Forestier", "count": 229, "highlighted": "…" },
          …
        ]
      },
      …
    ]
  }
}

# Field-by-field map for an Algolia-emulating client

What Algolia returns Where to read it in Skryx
response.facets response.facets (identical dict shape)
response.facets.<field> response.facets.<field>
response.facets_stats (not exposed today — see roadmap)
response.facetHits (SFFV) (not the same feature — see note below)
response.hits response.hits
response.nbHits response.found
response.page / response.nbPages response.page / response.total_pages
response.hitsPerPage response.per_page
response.processingTimeMS response.search_time_ms
response.query echo from request.q

If your transformer was originally written for Algolia and you see empty facets after switching to Skryx, the usual culprits are:

  • Reading from response.data.facet_counts and trying to index it like a dict — it's an array of { field_name, counts: [...] }, not { field_name: { value: count } }. Use response.facets instead; that's the dict version.
  • Looking for facetHits — that's Algolia's search-for-facet-values feature (a separate endpoint where you search inside a facet's values). Skryx returns facet aggregations under facets, not facetHits.
  • Looking for facetCounts (camelCase). Skryx is snake_case, and the dict lives at facets anyway.

# Drop-in shim

A 4-line transformer that maps a Skryx response to the exact Algolia response shape (good enough for most InstantSearch widgets):

function skryxToAlgolia(r) {
  return {
    hits:               r.hits,
    nbHits:             r.found,
    page:               r.page - 1,                  // Algolia is 0-indexed
    nbPages:            r.total_pages,
    hitsPerPage:        r.per_page,
    processingTimeMS:   r.search_time_ms,
    facets:             r.facets,                    // identical shape
    query:              r._original_query ?? '',
  };
}

# Zero-downtime reindex

When you need to rebuild the index from scratch — schema change, full re-import from your source of truth, big taxonomy redesign — never wipe the live index. Build a temp, populate it, then atomically swap.

Skryx ships this as two endpoints that compose:

The five-step pattern:

# 1. Create the temp index with whatever shape you need.
curl -X POST https://api.skryx.io/v1/indexes \
  -H "Authorization: Bearer $SKRYX_API_KEY" \
  -d '{ "name": "products_tmp", "schema": { "fields": [ ... ] } }'

# 2. Push the new data in batches of 1,000. Parallelise 4-8 wide.
curl -X POST https://api.skryx.io/v1/indexes/products_tmp/documents/batch \
  -H "Authorization: Bearer $SKRYX_API_KEY" \
  -d '{ "documents": [ ... ] }'
# ... repeat until all batches are in ...

# 3. Mirror the synonyms / ranking / settings from the live index.
curl -X POST https://api.skryx.io/v1/indexes/products_tmp/copy-settings-from \
  -H "Authorization: Bearer $SKRYX_API_KEY" \
  -d '{ "source_index": "products", "copy": ["synonyms","ranking_rules","settings"] }'

# 4. Atomic alias swap — search traffic now serves the new data.
#    drop_source_after=true also deletes the displaced old data.
curl -X POST https://api.skryx.io/v1/indexes/products/swap-with \
  -H "Authorization: Bearer $SKRYX_API_KEY" \
  -d '{ "target_index": "products_tmp", "drop_source_after": true }'

Steps 1 to 3 happen on a side index — your live search keeps serving the old data through all of it. Step 4 cuts over in a single atomic alias swap inside the Skryx engine. Visitors see no gap, no empty index, no 503.

If you skip drop_source_after, the displaced data stays reachable under the temp name — useful for verifying the new index before deleting old data, or for an instant rollback (just call swap-with again with the names reversed).

# How an UTB-style import job looks in code

The catalog is large (25,000+ rows) and lives in MySQL. Push it page by page:

$offset = 0;
$batchSize = 1000;

while (true) {
    $batch = Product::where('deleted_at', null)
        ->orderBy('id')
        ->offset($offset)->limit($batchSize)
        ->get()
        ->map(fn ($p) => $p->toSkryxDocument())
        ->all();

    if (empty($batch)) break;

    $skryx->post('/v1/indexes/products_tmp/documents/batch', [
        'documents' => $batch,
        'action' => 'upsert',
    ]);

    $offset += $batchSize;
}

// Inherit synonyms, ranking rules, settings from the live index.
$skryx->post('/v1/indexes/products_tmp/copy-settings-from', [
    'source_index' => 'products',
    'copy' => ['synonyms', 'ranking_rules', 'settings'],
]);

// Atomic cutover.
$skryx->post('/v1/indexes/products/swap-with', [
    'target_index' => 'products_tmp',
    'drop_source_after' => true,
]);

For 25k products this whole flow runs in 3–4 minutes — most of it is the batch upserts. Cutover itself is single-digit milliseconds.

# What changes for the better

  • Predictable flat pricing. No surprise per-search bills.
  • Auto-sync from a feed URL. Skip the ETL job.
  • AI Query Understanding and AI Coach included.
  • EU-hosted. Latency drops if your customers are in Europe.

# What you give up

Skryx doesn't currently offer:

  • Algolia's hosted personalisation product.
  • The aroundLatLng geo-search ranking (a planned addition — open an issue if you need it sooner).

For everything else, the parity is honest. If you hit a gap, email hello@skryx.io — we'll tell you if it's on the roadmap.


# Interactive migration helper

esc