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_countsand trying to index it like a dict — it's an array of{ field_name, counts: [...] }, not{ field_name: { value: count } }. Useresponse.facetsinstead; 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 underfacets, notfacetHits. - Looking for
facetCounts(camelCase). Skryx is snake_case, and the dict lives atfacetsanyway.
# 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:
POST /v1/indexes/{name}/copy-settings-fromcopies synonyms, ranking rules and search settings from one index to another.POST /v1/indexes/{name}/swap-withatomically repoints the live alias at the temp index's data.
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
aroundLatLnggeo-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.