Skip to main content
This page aims to help current users of Elasticsearch make the transition to Meilisearch. For a high-level comparison of the two search engines, see Meilisearch vs Elasticsearch.

Overview

This guide walks you through exporting documents from an Elasticsearch index and importing them into Meilisearch using a script in JavaScript, Python, or Ruby. You can also skip directly to the finished script. The migration process consists of four steps:
  1. Export your data from Elasticsearch
  2. Prepare your data for Meilisearch
  3. Import your data into Meilisearch
  4. Configure your Meilisearch index settings (optional)
To help with the transition, this guide also includes a comparison of settings and parameters, query types, and API methods. Before continuing, make sure you have Meilisearch installed and have access to a command-line terminal. If you’re unsure how to install Meilisearch, see our quick start.
This guide includes examples in JavaScript, Python, and Ruby. The packages used:

Export your Elasticsearch data

Initialize project

mkdir elastic-meilisearch-migration
cd elastic-meilisearch-migration
npm init -y
touch script.js

Install dependencies

npm install -s @elastic/elasticsearch meilisearch

Create Elasticsearch client

You need your Elasticsearch host URL and authentication credentials. Paste the below code in your script:
const { Client } = require("@elastic/elasticsearch");

const esClient = new Client({
  node: "ELASTICSEARCH_URL",
  auth: {
    // Use API key authentication:
    apiKey: "ELASTICSEARCH_API_KEY",
    // Or use basic authentication:
    // username: "USERNAME",
    // password: "PASSWORD",
  },
});
Replace ELASTICSEARCH_URL with your Elasticsearch cluster URL (for example, https://localhost:9200) and provide your authentication credentials.

Fetch data from Elasticsearch

Use the Point in Time API with search_after to paginate through all documents in the index. This approach is recommended over the deprecated Scroll API.
const INDEX_NAME = "YOUR_INDEX_NAME";
const BATCH_SIZE = 10000;

async function fetchAllDocuments() {
  const records = [];

  // Open a Point in Time
  const pit = await esClient.openPointInTime({
    index: INDEX_NAME,
    keep_alive: "5m",
  });

  let searchAfter;
  while (true) {
    const response = await esClient.search({
      body: {
        size: BATCH_SIZE,
        query: { match_all: {} },
        pit: { id: pit.id, keep_alive: "5m" },
        sort: [{ _doc: "asc" }],
        ...(searchAfter && { search_after: searchAfter }),
      },
    });

    const hits = response.hits.hits;
    if (hits.length === 0) break;

    records.push(...hits);
    searchAfter = hits[hits.length - 1].sort;
  }

  // Close the Point in Time
  await esClient.closePointInTime({ id: pit.id });
  return records;
}
Replace YOUR_INDEX_NAME with the name of the Elasticsearch index you want to migrate.

Prepare your data

Elasticsearch documents are wrapped in metadata (_id, _index, _source). You need to extract the document data from _source and ensure each document has a valid primary key for Meilisearch.
function prepareDocuments(hits) {
  return hits.map((hit) => {
    const doc = hit._source;
    doc.id = hit._id;
    return doc;
  });
}
Meilisearch stores documents as flat JSON objects. If your Elasticsearch documents use nested objects or the nested mapping type, you must flatten them before indexing. For example, { "author": { "name": "John" } } should become { "author_name": "John" } or kept as-is if you only need it for display purposes. Only top-level fields can be used for filtering, sorting, and searching.

Handle geo data

If your Elasticsearch documents use geo_point fields, convert them to Meilisearch’s _geo format:
function convertGeoFields(doc, geoFieldName) {
  if (doc[geoFieldName]) {
    const geo = doc[geoFieldName];
    doc._geo = {
      lat: geo.lat,
      lng: geo.lon, // Elasticsearch uses "lon", Meilisearch uses "lng"
    };
    delete doc[geoFieldName];
  }
  return doc;
}

Import your data into Meilisearch

Create Meilisearch client

Create a Meilisearch client by passing the host URL and API key of your Meilisearch instance. The easiest option is to use the automatically generated admin API key.
const { MeiliSearch } = require("meilisearch");

const meiliClient = new MeiliSearch({
  host: "MEILI_HOST",
  apiKey: "MEILI_API_KEY",
});
const meiliIndex = meiliClient.index("MEILI_INDEX_NAME");
Replace MEILI_HOST, MEILI_API_KEY, and MEILI_INDEX_NAME with your Meilisearch host URL, API key, and target index name. Meilisearch will create the index if it doesn’t already exist.

Upload data to Meilisearch

Use the Meilisearch client method addDocumentsInBatches to upload all records in batches of 100,000.
const UPLOAD_BATCH_SIZE = 100000;
await meiliIndex.addDocumentsInBatches(documents, UPLOAD_BATCH_SIZE);
When you’re ready, run the script:
node script.js

Finished script

const { Client } = require("@elastic/elasticsearch");
const { MeiliSearch } = require("meilisearch");

const ES_INDEX = "YOUR_INDEX_NAME";
const FETCH_BATCH_SIZE = 10000;
const UPLOAD_BATCH_SIZE = 100000;

(async () => {
  // Connect to Elasticsearch
  const esClient = new Client({
    node: "ELASTICSEARCH_URL",
    auth: {
      apiKey: "ELASTICSEARCH_API_KEY",
    },
  });

  // Fetch all documents using Point in Time
  const records = [];
  const pit = await esClient.openPointInTime({
    index: ES_INDEX,
    keep_alive: "5m",
  });

  let searchAfter;
  while (true) {
    const response = await esClient.search({
      body: {
        size: FETCH_BATCH_SIZE,
        query: { match_all: {} },
        pit: { id: pit.id, keep_alive: "5m" },
        sort: [{ _doc: "asc" }],
        ...(searchAfter && { search_after: searchAfter }),
      },
    });

    const hits = response.hits.hits;
    if (hits.length === 0) break;

    records.push(...hits);
    searchAfter = hits[hits.length - 1].sort;
  }

  await esClient.closePointInTime({ id: pit.id });

  // Prepare documents for Meilisearch
  const documents = records.map((hit) => {
    const doc = hit._source;
    doc.id = hit._id;
    return doc;
  });

  console.log(`Fetched ${documents.length} documents from Elasticsearch`);

  // Upload to Meilisearch
  const meiliClient = new MeiliSearch({
    host: "MEILI_HOST",
    apiKey: "MEILI_API_KEY",
  });
  const meiliIndex = meiliClient.index("MEILI_INDEX_NAME");

  await meiliIndex.addDocumentsInBatches(documents, UPLOAD_BATCH_SIZE);
  console.log("Migration complete");
})();

Configure your index settings

Meilisearch’s default settings deliver relevant, typo-tolerant search out of the box. However, if your Elasticsearch index relies on specific mappings or analyzers, you may want to configure equivalent Meilisearch settings. To customize your index settings, see configuring index settings. To understand the differences between Elasticsearch and Meilisearch settings, read on.

Key conceptual differences

Elasticsearch uses explicit mappings to define how each field is indexed, analyzed, and stored. You must configure analyzers, tokenizers, and field types before indexing data. Search behavior is controlled through a complex Query DSL. Meilisearch takes a different approach: all fields are automatically indexed and searchable by default. You refine behavior through index settings (which affect all searches) and search parameters (which affect a single query). Features like typo tolerance, prefix search, and ranking work out of the box without configuration. This means many Elasticsearch configurations have no direct equivalent in Meilisearch because the behavior is automatic. For example, you don’t need to configure analyzers for typo tolerance, prefix matching, or stop words — Meilisearch handles these by default.

Settings and parameters comparison

The below tables compare Elasticsearch mappings, settings, and query parameters with the equivalent Meilisearch features.

Index mappings and field configuration

ElasticsearchMeilisearchNotes
mappings.properties (field types)AutomaticMeilisearch infers field types automatically
properties.*.type: "text"searchableAttributesAll fields are searchable by default; use this setting to restrict or reorder
properties.*.type: "keyword"filterableAttributesAdd fields you want to filter or facet on
properties.*.index: falsedisplayedAttributesControl which fields appear in results
properties.*.type: "geo_point"_geo field with lat/lngAdd _geo to filterableAttributes and sortableAttributes
properties.*.type: "nested"Flatten to top-level fieldsMeilisearch does not support nested object queries
_source.excludesdisplayedAttributesOnly list the fields you want returned
enabled: falseOmit from searchableAttributesFields are still stored but not searched

Analysis and text processing

ElasticsearchMeilisearchNotes
analysis.analyzerAutomaticMeilisearch uses a built-in language-aware analyzer
analysis.tokenizerseparatorTokens / nonSeparatorTokensCustomize word boundary behavior
analysis.filter.stopstopWordsDefine words to ignore during search
analysis.filter.synonymsynonymsDefine equivalent terms
analysis.filter.stemmerAutomaticBuilt-in stemming via language detection
settings.index.analysis.normalizerAutomaticMeilisearch normalizes Unicode, casing, and diacritics automatically
Language-specific analyzerslocalizedAttributesAssign languages to specific fields

Search query parameters

ElasticsearchMeilisearchNotes
query.match / query.multi_matchq search paramMeilisearch searches all searchableAttributes by default
query.term / query.termsfilter search paramUse filter expressions for exact matches
query.bool.filterfilter search paramSupports AND, OR, NOT, () operators
query.bool.must / should / must_notfilter + qCombine search query with filter expressions
query.rangefilter search paramUse operators like field > value or field value1 TO value2
query.fuzzy / fuzzinessAutomaticBuilt-in typo tolerance, configurable per index
query.prefixAutomaticBuilt-in prefix search on the last query word
query.knnhybrid + vector search paramsRequires embedders setting
query.geo_distance_geoRadius(lat, lng, radius) in filterRequires _geo in filterableAttributes
query.geo_bounding_box_geoBoundingBox([lat, lng], [lat, lng]) in filterRequires _geo in filterableAttributes
highlightattributesToHighlight + highlightPreTag + highlightPostTagSearch params
_sourceattributesToRetrieveSearch param
from / sizeoffset / limit or page / hitsPerPageSearch params
sortsort search paramRequires sortableAttributes setting
search_afteroffset / limit or page / hitsPerPageMeilisearch uses simpler pagination
aggs (aggregations)facets search paramReturns value counts; complex aggregations are not supported
explainshowRankingScore / showRankingScoreDetailsSearch params
collapsedistinct search param or distinctAttribute settingField-level deduplication
min_scorerankingScoreThresholdSearch param

Index settings

ElasticsearchMeilisearchNotes
index.number_of_replicasAutomatic (Meilisearch Cloud)Meilisearch Cloud handles replication
index.number_of_shardsAutomatic (Meilisearch Cloud)Meilisearch Cloud handles sharding
index.max_result_windowpagination.maxTotalHitsDefault is 1000 in Meilisearch
index.refresh_intervalAutomaticMeilisearch indexes asynchronously via tasks

What you can simplify

Many Elasticsearch configurations become unnecessary when migrating to Meilisearch:
  • Analyzers and tokenizers — Meilisearch’s built-in text processing handles tokenization, normalization, stemming, and language detection automatically.
  • Mapping definitions — Field types are inferred. You don’t need to define mappings before indexing documents.
  • Replicas and shards — Meilisearch Cloud manages these automatically. Self-hosted instances run as a single process.
  • Index lifecycle management — Meilisearch doesn’t require index rotation, rollover policies, or shard management.
  • Query complexity — Most Elasticsearch bool queries with nested must, should, and filter clauses translate to a simple q parameter combined with a filter string.

Query comparison

This section shows how common Elasticsearch queries translate to Meilisearch. Elasticsearch:
{
  "query": {
    "match": {
      "title": "search engine"
    }
  }
}
Meilisearch:
{
  "q": "search engine"
}
Meilisearch searches all searchableAttributes by default. To restrict to a specific field, use the attributesToSearchOn search parameter.

Filtering

Elasticsearch:
{
  "query": {
    "bool": {
      "must": { "match": { "title": "search" } },
      "filter": [
        { "term": { "status": "published" } },
        { "range": { "price": { "gte": 10, "lte": 50 } } }
      ]
    }
  }
}
Meilisearch:
{
  "q": "search",
  "filter": "status = published AND price >= 10 AND price <= 50"
}
Attributes used in filter must first be added to filterableAttributes.

Sorting

Elasticsearch:
{
  "query": { "match_all": {} },
  "sort": [
    { "price": "asc" },
    { "date": "desc" }
  ]
}
Meilisearch:
{
  "q": "",
  "sort": ["price:asc", "date:desc"]
}
Attributes used in sort must first be added to sortableAttributes.
Elasticsearch:
{
  "query": { "match": { "title": "shoes" } },
  "aggs": {
    "colors": { "terms": { "field": "color" } },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 50 },
          { "from": 50, "to": 100 },
          { "from": 100 }
        ]
      }
    }
  }
}
Meilisearch:
{
  "q": "shoes",
  "facets": ["color", "price"]
}
Meilisearch returns value distributions for each facet. Range aggregations are not supported — use filter to narrow results by range. Elasticsearch:
{
  "query": {
    "geo_distance": {
      "distance": "10km",
      "location": { "lat": 48.8566, "lon": 2.3522 }
    }
  },
  "sort": [
    { "_geo_distance": { "location": { "lat": 48.8566, "lon": 2.3522 }, "order": "asc" } }
  ]
}
Meilisearch:
{
  "filter": "_geoRadius(48.8566, 2.3522, 10000)",
  "sort": ["_geoPoint(48.8566, 2.3522):asc"]
}
The _geo attribute must be added to both filterableAttributes and sortableAttributes.

API methods

This section compares Elasticsearch and Meilisearch API operations.
OperationElasticsearchMeilisearch
Create indexPUT /my-indexPOST /indexes
Delete indexDELETE /my-indexDELETE /indexes/{index_uid}
Get index infoGET /my-indexGET /indexes/{index_uid}
List indexesGET /_cat/indicesGET /indexes
Index documentPOST /my-index/_docPOST /indexes/{index_uid}/documents
Bulk indexPOST /_bulkPOST /indexes/{index_uid}/documents (accepts arrays)
Get documentGET /my-index/_doc/{id}GET /indexes/{index_uid}/documents/{id}
Delete documentDELETE /my-index/_doc/{id}DELETE /indexes/{index_uid}/documents/{id}
Delete by queryPOST /my-index/_delete_by_queryPOST /indexes/{index_uid}/documents/delete
SearchPOST /my-index/_searchPOST /indexes/{index_uid}/search
Multi-searchPOST /_msearchPOST /multi-search
Get settingsGET /my-index/_settingsGET /indexes/{index_uid}/settings
Update settingsPUT /my-index/_settingsPATCH /indexes/{index_uid}/settings
Create API keyPOST /_security/api_keyPOST /keys
Get cluster healthGET /_cluster/healthGET /health
Get task statusGET /_tasks/{task_id}GET /tasks/{task_uid}

Front-end components

Elasticsearch offers Search UI, a React component library for building search interfaces. Meilisearch is compatible with Algolia’s InstantSearch libraries through Instant Meilisearch. InstantSearch provides a rich set of pre-built widgets for search boxes, hits, facets, pagination, and more. You can find an up-to-date list of the components supported by Instant Meilisearch in the GitHub project’s README.