Elastic Cloud → Meilisearch
Copy an Elastic Cloud index into Meilisearch with a point-in-time read, put _id on the document, and flatten nested fields. Analyzers and cluster security do not come with the documents.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Elastic Cloud
- Managed Elasticsearch delivers powerful full-text, vector, analytics, and observability capabilities at scale
- Managed indexing, relevance, filtering, and globally available query APIs reduce search operations
- Mappings, analyzers, cluster sizing, upgrades, and query complexity require search expertise
- 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: Mappings, analyzers, cluster sizing, upgrades, and query complexity require search expertise.
What you lose: Managed Elasticsearch delivers powerful full-text, vector, analytics, and observability capabilities at scale. 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
- The source is an Elasticsearch index on Elastic Cloud, reached with the Elasticsearch client.
- You want application search, not a copy of Kibana, ingest pipelines, or index lifecycle policies.
- You can run Meilisearch or Meilisearch Cloud beside Elastic Cloud until queries match well enough.
- The site already documents Elastic Cloud → Algolia and Elastic Cloud → Typesense Cloud.
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 |
|---|---|---|---|---|
| Documents | partial | critical | Meilisearch's guide pages an index with the Point in Time API and search_after, which it prefers to the deprecated Scroll API. It reads hits in batches of 10,000, then uploads with addDocumentsInBatches in batches of 100,000. | Count documents on both sides. Re-run is safe if the same primary key overwrites. |
| Primary key | manual | critical | Elasticsearch wraps the document in _id, _index, and _source. Meilisearch needs a primary key on the document. The guide copies _id onto id. | Do that copy before upload. A missing id collapses updates. |
| Nested objects | partial | critical | Meilisearch stores flat JSON. Nested mappings have to be flattened. Only top-level fields can be filtered, sorted, or searched. The guide's example turns author.name into author_name, or leaves the object for display only. | List every nested field you filter on and flatten those before the full upload. |
| Keyword and text mappings | partial | high | Meilisearch infers types and searches all fields unless you set searchableAttributes. Keyword fields you want to facet belong in filterableAttributes. index: false maps to displayedAttributes in their comparison table. | Set filterable attributes before you judge facets. A filter on a field that is not filterable will fail. |
| Analyzers | lost | high | Meilisearch says custom analyzers and tokenizers are not copied. Stemming, Unicode, and language detection are built in. Stop words, synonyms, and separator tokens are the settings you recreate. Localized fields use localizedAttributes. | Run the queries you care about on both engines. Do not expect the same ranking. |
| Query DSL | manual | critical | Bool queries with must, should, and filter become a q string plus a filter. geo_point becomes a _geo field with lat and lng, and it must be filterable and sortable for radius and bounding-box filters. | Rewrite each production query. Keep Elastic Cloud serving until the new queries pass. |
| Elastic Cloud auth | manual | high | The Kestra guide tells you to add basic auth for Elastic Cloud, with the username elastic and the password in a secret. The open migration script will not reach a locked cluster without credentials. | Use a key you can revoke. Do not put the elastic password in the app that serves search. |
| Shards, replicas, and ILM | lost | medium | Meilisearch's comparison says replicas, shards, and index lifecycle do not have the same knobs. Meilisearch Cloud manages that layer. A self-hosted process is one process. | Size Meilisearch for the index you measured. Do not copy shard counts across. |
| Security roles and API keys | manual | critical | Elastic Cloud roles do not become Meilisearch keys. The guide uses the admin key for the load, which is the wrong key for the public app. | Create a search-only key for the app. Revoke the admin key from any client bundle. |
| Deletes during dual-run | partial | high | The Kestra guide says a one-shot copy ignores later deletes. If both sides run for a while, index each fresh backfill into a new Meilisearch index and move an alias, so missing documents disappear. | Either freeze writes for the copy, or use a new index plus an alias at cutover. |
| Kibana and pipelines | lost | medium | The migration guide moves documents and compares search settings. It does not move dashboards, alerts, or ingest pipelines. | Keep Elastic Cloud if you still need those. This move is the search index. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| _source | Meilisearch document | transform | Drop the Elasticsearch wrapper. |
| _id | id | transform | Copy it onto the document before upload. |
| nested object | Top-level fields | transform | Flatten anything you filter or sort. |
| keyword field | filterableAttributes | manual | Add it before facet queries. |
| text field | searchableAttributes | manual | All fields are searchable until you restrict them. |
| geo_point | _geo | transform | lat and lng. Mark it filterable and sortable. |
| synonyms and stop words | synonyms and stopWords | manual | Recreate the lists. Analyzers do not import. |
| bool query | q and filter | transform | Rewrite production queries one by one. |
| Elastic Cloud credentials | Client basic auth or an API key | manual | Kestra's note uses username elastic. Revoke it after the load. |
Make the move recoverable.
Create the source-of-truth backup
Keep Elastic Cloud serving and keep a snapshot of the index you copied.
- Record index names, mappings, synonyms, and the queries in production.
- Confirm you can open a point in time against the Elastic Cloud endpoint.
- Note document counts per index.
- Keep Elastic Cloud as the live engine until the Meilisearch pilot matches.
- Store the API key in a secret manager, not in the migration script you commit.
Proof to capture: You have counts, mappings, and a revoked-key plan before the first full upload.
Document load
Read the Elastic Cloud index and add documents to Meilisearch.
- Point the Elasticsearch client at Elastic Cloud with basic auth or an API key.
- Page with a point in time and search_after, 10,000 hits at a time.
- Copy _id to id and flatten nested fields you must filter.
- Upload in batches of 100,000 and wait until the task succeeds.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Settings and cutover
Make facets and queries behave, then switch reads.
- Set filterableAttributes, searchableAttributes, synonyms, and stop words.
- Run the same queries against both engines.
- During an overlap, follow Kestra's advice: backfill on a schedule, or filter on updated_at.
- Cut reads over by pointing an alias at a fresh index if deletes happened during the overlap.
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.
Documents have no stable id
_id stayed in the hit metadata and never landed on the document.
- Consequence
- Updates insert duplicates or Meilisearch rejects the batch.
- Mitigation
- Map _id to id in the prepare step the guide shows.
Stop if: A second run changes the document count when it should only overwrite.
Facets query fields that are not filterable
A keyword mapping was never added to filterableAttributes.
- Consequence
- Filters error or silently fail to narrow results.
- Mitigation
- Add the attribute, then re-run the query.
Stop if: A production filter has no filterable attribute.
Reads move before relevance is checked
The index is full and nobody compared a real query.
- Consequence
- Ranking changes for the queries that pay the bills.
- Mitigation
- Dual-run behind a flag, as the Kestra guide describes.
Stop if: You have not compared the top queries.
Do the work in this order.
- Days 1 to 3 · inventory
Mappings and queries
4 to 8 hours active2 to 3 days elapsedOwner picks the query list waiting- Export mappings and the production queries.
- Mark nested fields and geo points.
Depends on: Elastic Cloud credentials and a Meilisearch admin key
Stop / go checkpointCopy documents?
Go when: Each filter field has a flatten or filterable plan.
Stop when: You still need Kibana or ingest pipelines from this move.
- Days 3 to 4 · backup
Counts and keys
2 to 4 hours active1 day elapsedNone waiting- Record document counts.
- Store credentials outside the repo.
Depends on: Inventory
Stop / go checkpointPilot index?
Go when: You can open a point in time.
Stop when: Auth to Elastic Cloud fails.
- Days 4 to 10 · pilot
One index beside production
8 to 20 hours active4 to 7 days elapsedIndexing tasks waiting- Load a pilot index.
- Set filterable attributes.
- Compare the top queries.
Depends on: Counts
Stop / go checkpointDual-run?
Go when: Counts match and the query list is acceptable.
Stop when: Nested filters still fail.
- Days 10 to 12 · cutover
Switch reads
3 to 6 hours active1 to 2 days elapsedAlias move waiting- Final backfill into a fresh index if deletes matter.
- Point the app at Meilisearch.
- Keep Elastic Cloud read-only for a rollback read.
Depends on: Query sign-off
Stop / go checkpointMeilisearch serves search?
Go when: Production queries hit Meilisearch only.
Stop when: A top query regresses.
- Days 12 to 19 · observe
Watch relevance
2 to 4 hours active7 days elapsedReal queries waiting- Sample zero-result queries.
- Keep the Elastic Cloud index until you accept ranking.
Depends on: Cutover
Stop / go checkpointRetire the index?
Go when: A week of queries is acceptable and counts still match.
Stop when: You need the old ranking back.
Cut over with a way back.
Cutover
The app reads Meilisearch. Elastic Cloud is still there if ranking is wrong.
- Freeze writes or start the alias-based backfill.
- Confirm document counts.
- Point search reads at Meilisearch.
- Watch errors on filterable attributes.
- Leave Elastic Cloud queryable until the observation window ends.
Proof to capture: Production search hits Meilisearch, counts match, and the top queries were compared.
Rollback
Point search reads back at Elastic Cloud.
- Flip the flag or client back to Elastic Cloud.
- Stop Meilisearch writes if you were dual-writing.
- Keep the Meilisearch index for the next attempt.
- Revoke any temporary elastic user password you created for the load if you are done with it.
- Re-run the query list against Elastic Cloud.
Proof to capture: Search traffic is on Elastic Cloud again and the index you did not delete still answers.
- Document counts diverge
- A filter errors because an attribute is not filterable
- Ranking on a top query is unacceptable
- The public client is using the admin key
- Deletes during dual-run left stale documents and you did not swap the alias
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | Document totals | Compare the index count with Meilisearch stats. | They match after the task finishes. | Count note. | |
V-02Blocking | id equals _id | Fetch ten documents from both sides. | The Meilisearch primary key is the old _id. | Sample JSON. | |
V-03Blocking | Flattened filters | Filter on a field that used to be nested. | The filter returns the same ids you expect, or you have accepted display-only. | Query pair. | |
V-04Blocking | filterableAttributes | Run each production filter. | No error about a non-filterable attribute. | Query log. | |
V-05 | _geo | Run a radius query if you had geo_point. | _geo is filterable and sortable, or geo is out of scope. | Query result. | |
V-06Blocking | Search key | Call search with the app key and with the admin key removed from the client. | Search works. The admin key is not in the app. | Client config. | |
V-07Blocking | Top queries | Compare the query list on both engines. | An owner has accepted the differences. | Query sheet. | |
V-08Blocking | Elastic Cloud still answers | Run one query against the old index. | It returns hits until you delete the deployment. | Query response. |
Remove the scaffolding safely.
Safe after: A week of accepted Meilisearch queries and a matching document count.
- Revoke the Elastic Cloud credential used for the dump.
- Rotate the Meilisearch admin key if it was pasted into a script.
- Delete pilot indexes you did not alias.
- Keep Elastic Cloud until the rollback date you wrote down.
- Remove the dual-read flag after that.
- Review synonyms the next time ranking complaints show up.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Meilisearch: migrate from ElasticsearchAccessed 2026-11-03
- Meilisearch: migrate Elasticsearch with KestraAccessed 2026-11-03