Algolia → Meilisearch
Browse each Algolia index and add the hits to Meilisearch. Set the primary key yourself. objectID often works, until another field also ends in id, or the id contains a character Meilisearch rejects.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Algolia
- Exceptionally fast hosted search combines polished relevance controls with mature frontend tooling
- Managed indexing, relevance, filtering, and globally available query APIs reduce search operations
- Record, operation, and feature pricing can rise quickly while ranking configuration remains proprietary
- Ranking behavior, analyzers, schemas, and learned signals require careful revalidation after migration
Meilisearch
- Open-source typo-tolerant search with a small API and an optional managed cloud
- Managed indexing, relevance, filtering, and globally available query APIs reduce search operations
- Primary keys, ranking rules, and heavy update workloads need explicit setup that a hosted index used to hide
- Ranking behavior, analyzers, schemas, and learned signals require careful revalidation after migration
Meilisearch: Open-source typo-tolerant search with a small API and an optional managed cloud. This removes a major source-side concern: Record, operation, and feature pricing can rise quickly while ranking configuration remains proprietary.
What you lose: Exceptionally fast hosted search combines polished relevance controls with mature frontend tooling. What you inherit: Primary keys, ranking rules, and heavy update workloads need explicit setup that a hosted index used to hide.
Jump to a section
Know the shape of the move.
This timeline assumes
- You are moving search indexes, not Algolia Recommend or a personalization campaign.
- The site already documents Algolia → Typesense Cloud and Algolia → Elastic Cloud.
- Meilisearch is v1 or newer. The official script expects the v4 Algolia JavaScript client in its Node example.
- You can read the index with an Algolia admin key and write with a Meilisearch admin key.
- Algolia stays in place until the same queries have been compared on both engines.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Records | partial | critical | Meilisearch's guide browses an index with browseObjects and uploads hits with addDocumentsInBatches in batches of 100,000. That copies JSON. It does not copy index settings unless you set them afterwards. | Count hits per index before and after the task succeeds. |
| Primary key | manual | critical | Meilisearch infers a primary key from the first document by looking for one attribute whose name ends in id. Algolia sends objectID, which matches. If the hit also has categoryID or any second id field, inference fails with index_primary_key_multiple_candidates_found and that batch is not stored. | Create the index with primaryKey set to objectID. Do not rely on inference. |
| Document id characters | partial | critical | A document id must be an integer or a string of letters, digits, hyphens, and underscores. Meilisearch says one bad id fails the whole batch, because addition is atomic. | Scan objectIDs. Rewrite illegal characters and keep a map back to the Algolia id. |
| Primary key lock-in | manual | high | Meilisearch says you cannot change the primary key while documents are in the index. You delete the documents, change the key, and add them again. | Pick objectID, or a cleaned id, before the first successful add. |
| Searchable attributes and ranking | partial | critical | Meilisearch maps searchableAttributes directly and turns customRanking into rankingRules. Algolia's ranking tie-break list has no one-to-one copy. Results will not be byte-identical. | Set searchableAttributes in the same order you care about. Compare a saved query list before cutover. |
| Facets and replicas | partial | high | attributesForFaceting becomes filterableAttributes. Sort replicas are not separate indexes. You mark sortableAttributes and pass sort at query time. | List every replica and the sort it existed to serve. Add those attributes to sortableAttributes. |
| Synonyms | partial | medium | Meilisearch's comparison says two-way and one-way synonyms map. Alt corrections and placeholders do not. | Export synonyms and rewrite the ones with no equivalent. Symfony added a long synonym list, including yaml/yml, after the documents were in. |
| Query rules and personalization | lost | high | Rules that pin, hide, or redirect results, and Algolia personalization, are not in the document upload. | Export the rules you still need. Rebuild the few that change revenue queries, or drop them in writing. |
| Analytics | lost | medium | Meilisearch's settings table says Algolia click analytics is a separate Analytics API, not an index setting that copies over. | Export the searches you use for zero-result reports before you cancel Algolia. |
| InstantSearch UI | partial | high | Meilisearch says InstantSearch widgets need the Instant Meilisearch adapter. A hand-rolled Algolia client is a rewrite. Settings and search parameters are different objects. You cannot pass an index setting as a query parameter. | Swap the client in a branch and run the widget list you actually render. |
| Geo and snippets | partial | medium | aroundLatLng becomes a _geoRadius filter. aroundPrecision has no equivalent. Snippets become attributesToCrop plus cropLength. restrictHighlightAndSnippetArrays is not supported. | Rewrite geo queries and check cropped fields on a long document. |
| Indexing load | manual | high | In February 2023, schappim wrote on Hacker News that a 50,000-record catalog with constant inventory updates fell hours behind on Meilisearch, and that part of the app moved to Elastic. Meilisearch's reply said indexing had improved. Treat that report as a 2023 workload, then measure yours. | Replay a day of updates in staging and watch the task queue. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| Index | Meilisearch index | manual | One index per Algolia index unless you signed a merge. |
| Hit | Document | transform | browseObjects, then addDocuments. |
| objectID | Primary key | transform | Set primaryKey explicitly. Clean illegal characters. |
| searchableAttributes | searchableAttributes | transform | Order matters. |
| attributesForFaceting | filterableAttributes | transform | Strip filterOnly and searchable modifiers. |
| Replica sort | sortableAttributes plus a sort parameter | transform | No replica index. |
| customRanking | rankingRules | transform | desc(popularity) becomes popularity:desc. |
| Synonym | Synonym | transform | Two-way and one-way map. Placeholders do not. |
| Query rule | No destination | unsupported | Rebuild or drop. |
| InstantSearch widget | Instant Meilisearch | transform | Check the adapter's supported widgets. |
Make the move recoverable.
Create the source-of-truth backup
Keep a copy of every index's hits and settings before you point the app at Meilisearch.
- Export settings, synonyms, and rules from each Algolia index.
- Browse hits to a file and record the count.
- Save the query list you will use for comparison, including typos.
- Note which objectIDs contain characters outside letters, digits, hyphens, and underscores.
- Keep the Algolia app running.
Proof to capture: Each index has a hit count, a settings JSON, and a list of ids that need rewriting.
Browse and add
Copy hits without inferring the wrong primary key.
- Create the index with primaryKey objectID, or your cleaned id field.
- Browse the Algolia index.
- Rewrite illegal ids.
- Add documents and wait until the task status is succeeded.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Settings translation
Restore the filters and sorts the UI already sends.
- Set searchableAttributes, filterableAttributes, and sortableAttributes.
- Append custom ranking rules after Meilisearch's defaults.
- Import synonyms that have an equivalent.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
Inference picks the wrong id
Hits contain objectID and another field ending in id.
- Consequence
- The task fails and the batch is empty.
- Mitigation
- Set primaryKey yourself.
Stop if: The first task ends in index_primary_key_multiple_candidates_found.
One bad id drops the batch
An objectID contains a character Meilisearch rejects.
- Consequence
- None of the documents in that atomic batch are indexed.
- Mitigation
- Scan ids before the add.
Stop if: The error code is invalid_document_id.
Ranking is treated as identical
The app switches when counts match but the top hits differ.
- Consequence
- Merchandised queries regress.
- Mitigation
- Compare a saved query list on both engines.
Stop if: A revenue query's top result changed and nobody signed it.
Do the work in this order.
- Days 1–3 · inventory
Indexes and id rules
6–10 hours active2–3 days elapsedSearch owner waiting- List indexes, replicas, rules, and synonyms.
- Scan objectIDs for illegal characters and extra id fields.
Depends on: Algolia admin key and a Meilisearch host
Stop / go checkpointCopy hits?
Go when: The primary key is chosen.
Stop when: A revenue rule has no destination and no one will drop it.
- Days 3–5 · backup
Export settings and hits
4–8 hours active1–3 days elapsedBrowse runtime waiting- Save settings, synonyms, and rules.
- Write hit counts.
Depends on: Index list
Stop / go checkpointLoad staging?
Go when: Every index has a count and a settings file.
Stop when: Browse missed an index the app still queries.
- Days 5–12 · pilot
Stage the index
12–30 hours active4–8 days elapsedTask queue waiting- Add documents with an explicit primary key.
- Apply settings and compare the query list.
Depends on: Hit files
Stop / go checkpointShadow traffic?
Go when: Counts match and signed queries look acceptable.
Stop when: A batch failed on id inference or invalid ids.
- Days 12–16 · cutover
Switch the client
4–10 hours active1–3 days elapsedDeploy waiting- Point the app at Meilisearch.
- Keep the Algolia write path until the queue is caught up.
Depends on: Passed query review
Stop / go checkpointStop Algolia writes?
Go when: Production queries hit Meilisearch and the task queue is drained.
Stop when: Updates are still landing only in Algolia.
- Days 15–22 · observe
Watch updates
4–8 hours active4–7 days elapsedA day of real updates waiting- Compare indexing lag with the Algolia baseline.
- Keep Algolia until that lag is acceptable.
Depends on: Client switch
Stop / go checkpointClose rollback?
Go when: Updates keep up through a busy day.
Stop when: The task queue falls hours behind.
Cut over with a way back.
Cutover
Serve queries from Meilisearch after counts, ids, and a signed query list pass.
- Freeze the comparison set and re-browse Algolia for a final count.
- Add any new hits with the same primary key.
- Deploy the Instant Meilisearch adapter or the rewritten client.
- Watch the task queue through the next update batch.
- Leave the Algolia application in place.
Proof to capture: The app queries Meilisearch. Document counts match. Signed queries were reviewed.
Rollback
Point the client back at Algolia, which still has the records.
- Deploy the previous Algolia client.
- Confirm queries return Algolia hits.
- Stop Meilisearch writes so a later retry does not diverge.
- Keep the id map and the settings JSON.
- Write down which queries disagreed.
Proof to capture: Production search uses Algolia again.
- Primary key task failed
- Hit counts differ
- A signed query's top result changed without approval
- Update lag keeps growing
- The UI still calls an Algolia parameter Meilisearch rejects
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | Explicit primary key | Read the index primary key after creation. | It is objectID or the cleaned field you signed. | Index JSON. | |
V-02Blocking | Character scan | Reject objectIDs outside letters, digits, hyphens, and underscores. | Zero illegal ids, or a rewrite map. | Id report. | |
V-03Blocking | Hits per index | Compare Algolia nbHits with Meilisearch document count after the task succeeds. | Counts match. | Count sheet. | |
V-04Blocking | Filters and sorts | Run a faceted query and a replica sort. | filterableAttributes and sortableAttributes cover them. | Query log. | |
V-05Blocking | Saved queries | Compare the top hits for the signed list, including a typo. | Differences are listed and accepted. | Query diff. | |
V-06 | Known pairs | Search both sides of a synonym such as yml and yaml. | Pairs you kept return the same documents. | Synonym sample. | |
V-07Blocking | Widgets | Click every InstantSearch widget you still render. | The adapter supports it, or you replaced it. | UI notes. | |
V-08Blocking | Task queue | Apply a production-sized update batch and read task status. | Tasks succeed and lag stays inside the agreed bound. | Task log. |
Remove the scaffolding safely.
Safe after: A busy day of updates with Algolia still able to take the client back.
- Archive settings, synonym exports, and the id rewrite map.
- Rotate the Algolia admin key you used to browse.
- Keep the Algolia app through the rollback window.
- Remove unused replica indexes from the app config only after cutover holds.
- Document the Meilisearch host and primary key.
- Schedule a ranking review after the first merchandising change.
When the plan met reality.
First-hand accounts are preferred. Vendor case studies are labeled, and every note below is an editorial paraphrase—follow the link for the full context.
Javier Eguiluz writes that symfony.com search had already used Solr, Elasticsearch, Google Custom Search, and Algolia. They left Algolia because JSON configuration did not give enough control over parsing, and readers reported confusing or missing results. They skipped the Symfony bundle because it targets Doctrine, and used the PHP client. Long RST pages were split into one document per section. The document id is an xxh3 hash of the URL, including the fragment, so a reindex updates in place. They parsed 12,754 files into 85,146 documents in 17 seconds. The index was about 1.2 GB, and most queries took under 10 milliseconds. Synonyms include yaml with yml, and symphony with symfony. He thanks Meilisearch for help and for hosting the index.
- Split long pages into section documents when a whole-page hit is too vague for the query.
- Set a stable id from the canonical URL so a later index run overwrites instead of duplicating.
- The move was about indexing control and result quality, after Algolia was already in production.
- Meilisearch hosted the index. Eguiluz says so at the end of the post.
Meilisearch's case study says Qogita used Algolia and was satisfied with functionality and performance, then compared Meilisearch, Typesense, and Algolia. Relevancy was scored against Algolia's results, including typos and common misspellings. They self-hosted a proof of concept, then put production on Meilisearch Cloud because of cost and a small DevOps team. The article credits the Python library for the migration. Engineering director Ivo Silva is quoted on speed, relevancy, and local setup. Savings are called substantial, and the article does not give a number. The byline is Maya Shin at Meilisearch.
- Score a short list of real queries against current Algolia results before you pick the replacement.
- Decide self-hosted versus Meilisearch Cloud after the proof of concept. That is the order Qogita used.
- This is Meilisearch's case study. The quote and the savings wording come from them.
- Typesense was in the comparison, and Meilisearch Cloud is what they say shipped.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Meilisearch: migrate from AlgoliaAccessed 2026-11-01
- Meilisearch: primary keyAccessed 2026-11-01
- Algolia: export and import indicesAccessed 2026-11-01
- Symfony: search migration write-upAccessed 2026-11-01
- Hacker News: Meilisearch 1.0 indexing threadAccessed 2026-11-01