Skip to main content
This page aims to help current users of Qdrant make the transition to Meilisearch.

Overview

Qdrant is a vector similarity search engine. Meilisearch combines full-text search with vector search through its hybrid search feature, letting you replace a separate keyword search engine and vector database with a single system. This guide walks you through exporting points from a Qdrant collection 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 Qdrant
  2. Prepare your data for Meilisearch
  3. Import your data into Meilisearch
  4. Configure embedders and index settings
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 Qdrant data

Initialize project

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

Install dependencies

npm install -s @qdrant/js-client-rest meilisearch

Create Qdrant client

You need your Qdrant host URL and optionally an API key if your instance requires authentication.
const { QdrantClient } = require("@qdrant/js-client-rest");

const qdrantClient = new QdrantClient({
  url: "QDRANT_URL",
  // apiKey: "QDRANT_API_KEY", // if authentication is enabled
});
Replace QDRANT_URL with your Qdrant instance URL (for example, http://localhost:6333) and provide your API key if required.

Fetch data from Qdrant

Use the Scroll API to paginate through all points in a collection. This retrieves both payload data and vectors.
const COLLECTION_NAME = "YOUR_COLLECTION_NAME";
const BATCH_SIZE = 1000;

async function fetchAllPoints() {
  const records = [];
  let offset = null;

  while (true) {
    const response = await qdrantClient.scroll(COLLECTION_NAME, {
      limit: BATCH_SIZE,
      offset: offset,
      with_payload: true,
      with_vectors: true,
    });

    records.push(...response.points);

    if (!response.next_page_offset) break;
    offset = response.next_page_offset;
  }

  return records;
}
Replace YOUR_COLLECTION_NAME with the name of the Qdrant collection you want to migrate.
Set with_vectors: true if you want to keep your existing vectors. If you plan to let Meilisearch re-embed your documents using a configured embedder, you can set this to false to speed up the export.

Prepare your data

Qdrant points contain an id, a payload (key-value data), and one or more vectors. You need to extract the payload fields as top-level document fields for Meilisearch.

Choose your vector strategy

Before preparing your data, decide how you want to handle vectors:
  • Option A: Let Meilisearch re-embed (recommended) — Configure an embedder in Meilisearch and let it generate vectors automatically from your document content. This is simpler and keeps your vectors in sync with your data.
  • Option B: Keep existing vectors — Include your Qdrant vectors in the _vectors field of each document using a userProvided embedder. This avoids re-embedding costs but requires you to manage vector updates yourself.

Transform documents

function prepareDocuments(points, keepVectors = false) {
  return points.map((point) => {
    // Extract payload fields as top-level document fields
    const doc = { ...point.payload };
    doc.id = String(point.id);

    // Option B: keep existing vectors
    if (keepVectors && point.vector) {
      if (typeof point.vector === "object" && !Array.isArray(point.vector)) {
        // Named vectors: { "text-embedding": [...], "image-embedding": [...] }
        doc._vectors = point.vector;
      } else {
        // Single unnamed vector
        doc._vectors = { default: point.vector };
      }
    }

    return doc;
  });
}

Handle geo data

If your Qdrant payloads contain geo fields (objects with lat and lon), 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, // Qdrant 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 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 { QdrantClient } = require("@qdrant/js-client-rest");
const { MeiliSearch } = require("meilisearch");

const COLLECTION_NAME = "YOUR_COLLECTION_NAME";
const FETCH_BATCH_SIZE = 1000;
const UPLOAD_BATCH_SIZE = 100000;
const KEEP_VECTORS = false; // set to true to preserve existing vectors

(async () => {
  // Connect to Qdrant
  const qdrantClient = new QdrantClient({
    url: "QDRANT_URL",
  });

  // Fetch all points using Scroll API
  const records = [];
  let offset = null;

  while (true) {
    const response = await qdrantClient.scroll(COLLECTION_NAME, {
      limit: FETCH_BATCH_SIZE,
      offset: offset,
      with_payload: true,
      with_vectors: KEEP_VECTORS,
    });

    records.push(...response.points);

    if (!response.next_page_offset) break;
    offset = response.next_page_offset;
  }

  // Prepare documents for Meilisearch
  const documents = records.map((point) => {
    const doc = { ...point.payload };
    doc.id = String(point.id);

    if (KEEP_VECTORS && point.vector) {
      if (typeof point.vector === "object" && !Array.isArray(point.vector)) {
        doc._vectors = point.vector;
      } else {
        doc._vectors = { default: point.vector };
      }
    }

    return doc;
  });

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

  // 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

After importing your data, you need to configure Meilisearch to handle vector search. You also gain access to full-text search, typo tolerance, faceting, and other features that work automatically.

Configure embedders

One of the biggest differences between Qdrant and Meilisearch is how they handle vectors. With Qdrant, your application must compute vectors before indexing and searching. With Meilisearch, you configure an embedder once and Meilisearch handles all embedding automatically — both at indexing time and at search time. This means you can remove all embedding logic from your application code. Instead of calling an embedding API, computing vectors, and sending them to your search engine, you simply send documents and text queries to Meilisearch. Configure an embedder source such as OpenAI, HuggingFace, or a custom REST endpoint:
curl -X PATCH 'MEILI_HOST/indexes/MEILI_INDEX_NAME/settings' \
  -H 'Authorization: Bearer MEILI_API_KEY' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "embedders": {
      "default": {
        "source": "openAi",
        "apiKey": "OPENAI_API_KEY",
        "model": "text-embedding-3-small",
        "documentTemplate": "A document titled {{doc.title}}: {{doc.description}}"
      }
    }
  }'
The documentTemplate controls what text is sent to the embedding model. Adjust it to match the fields in your documents. Meilisearch will automatically embed all existing documents and keep vectors up to date as you add, update, or delete documents. For more options including HuggingFace models, Ollama, and custom REST endpoints, see configuring embedders.
If you prefer to keep your existing Qdrant vectors instead of re-embedding, you can export them (set with_vectors: true in the migration script) and configure a userProvided embedder:
curl -X PATCH 'MEILI_HOST/indexes/MEILI_INDEX_NAME/settings' \
  -H 'Authorization: Bearer MEILI_API_KEY' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "embedders": {
      "default": {
        "source": "userProvided",
        "dimensions": 1536
      }
    }
  }'
Replace 1536 with the vector dimension used in your Qdrant collection. With this approach, you remain responsible for computing and providing vectors when adding or updating documents. You also need to compute query vectors client-side when searching.If your Qdrant collection uses named vectors, create a separate embedder for each vector name. The embedder names in Meilisearch must match the keys used in the _vectors field of your documents.

Configure filterable and sortable attributes

In Qdrant, payload indexes must be created explicitly for filtering. In Meilisearch, configure filterableAttributes and sortableAttributes for the fields you want to filter and sort on:
curl -X PATCH 'MEILI_HOST/indexes/MEILI_INDEX_NAME/settings' \
  -H 'Authorization: Bearer MEILI_API_KEY' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "filterableAttributes": ["category", "price", "_geo"],
    "sortableAttributes": ["price", "date", "_geo"]
  }'

What you gain

Migrating from Qdrant to Meilisearch gives you several features that work out of the box:
  • No more client-side embedding — Configure an embedder once, then just send text queries. Meilisearch handles vectorization for both documents and searches
  • Full-text search with typo tolerance, prefix matching, and language-aware tokenization
  • Hybrid search combining keyword relevancy and semantic similarity in a single query — no need to orchestrate two search systems
  • Faceted search with value distributions for building filter UIs
  • Highlighting of matching terms in results
  • Synonyms and stop words support
  • Built-in ranking rules that combine text relevancy, semantic similarity, and custom sort attributes

Settings and parameters comparison

The below tables compare Qdrant concepts with their Meilisearch equivalents.

Core concepts

QdrantMeilisearchNotes
CollectionIndex
PointDocumentA Point is vector-first (vector + metadata payload). A Document is content-first (fields + optional vectors)
PayloadDocument fieldsPayload fields become top-level document fields
Vector_vectors field or auto-generated via embeddersMeilisearch can auto-generate vectors from document content, so importing vectors is optional
Point ID (uuid or integer)Document id (string)Must convert to string
Named vectorsMultiple embeddersOne embedder per vector name
Collection aliasesIndex swapAtomic swap of two indexes

Indexing and storage

QdrantMeilisearchNotes
payload_schema / payload indexfilterableAttributesRequired for filtering
HNSW index configAutomatic (DiskANN-based)No manual tuning needed
Quantization (scalar, product, binary)Built-in binary quantization via embeddersConfigured per embedder
on_disk storageAutomaticMeilisearch uses memory-mapped storage
Sharding / replicationAutomatic (Meilisearch Cloud)
SnapshotsDumps / Snapshots

Search parameters

QdrantMeilisearchNotes
query (precomputed vector)q + hybrid with auto-embedderMeilisearch embeds the query for you — no client-side vector computation
No built-in full-text searchq search paramFull-text search with typo tolerance, works standalone or combined with hybrid
No equivalenthybrid.semanticRatioTune the balance between keyword and semantic results (0.0–1.0)
filter.mustfilter with AND
filter.shouldfilter with OR
filter.must_notfilter with NOT
filter.match (exact value)filter with = operator
filter.range (gt, gte, lt, lte)filter with >, >=, <, <= or TO
filter.geo_bounding_box_geoBoundingBox([lat, lng], [lat, lng]) in filter
filter.geo_radius_geoRadius(lat, lng, radius) in filter
with_payloadattributesToRetrieveSearch param
score_thresholdrankingScoreThresholdSearch param
limitlimitSearch param
offsetoffsetSearch param
with_vectorsretrieveVectorsSearch param
No equivalentattributesToHighlightHighlight matching terms in results
No equivalentfacetsGet value distributions for fields
No equivalentsortSort by attributes (requires sortableAttributes)
No equivalentattributesToCropExcerpt matching content

Query comparison

This section shows how common Qdrant queries translate to Meilisearch. All Meilisearch examples below assume you have configured an auto-embedder — you simply send a text query and Meilisearch handles embedding automatically. No need to compute vectors client-side. With Qdrant, you must compute the query vector yourself before searching. With Meilisearch, you just send a natural language query: Qdrant:
POST /collections/my_collection/points/search
{
  "vector": [0.1, 0.2, 0.3, ...],
  "limit": 10
}
Meilisearch:
POST /indexes/my_index/search
{
  "q": "comfortable running shoes",
  "hybrid": {
    "semanticRatio": 1.0,
    "embedder": "default"
  },
  "limit": 10
}
With an auto-embedder configured, Meilisearch embeds the q text for you. Setting semanticRatio to 1.0 performs pure semantic search, just like Qdrant — but without managing vectors in your application code.

Hybrid search (keyword + semantic)

This is Meilisearch’s biggest advantage over Qdrant. A single query combines typo-tolerant keyword matching with semantic similarity — something that would require two separate systems with Qdrant: Meilisearch:
POST /indexes/my_index/search
{
  "q": "comfortable running shoes",
  "hybrid": {
    "semanticRatio": 0.5,
    "embedder": "default"
  }
}
A semanticRatio of 0.5 gives equal weight to keyword and semantic results. Adjust this value to tune the balance: closer to 0.0 favors keyword matching, closer to 1.0 favors semantic similarity. Qdrant:
POST /collections/my_collection/points/search
{
  "vector": [0.1, 0.2, 0.3, ...],
  "filter": {
    "must": [
      { "key": "category", "match": { "value": "electronics" } },
      { "key": "price", "range": { "lte": 500 } }
    ]
  },
  "limit": 10
}
Meilisearch:
POST /indexes/my_index/search
{
  "q": "wireless headphones",
  "filter": "category = electronics AND price <= 500",
  "hybrid": {
    "semanticRatio": 0.7,
    "embedder": "default"
  },
  "limit": 10
}
No need to compute a vector for “wireless headphones” — Meilisearch handles it. The filter syntax is also simpler: a single string instead of nested JSON objects.
Attributes used in filter must first be added to filterableAttributes.
Qdrant:
POST /collections/my_collection/points/search
{
  "vector": [0.1, 0.2, 0.3, ...],
  "filter": {
    "must": [
      {
        "key": "location",
        "geo_radius": {
          "center": { "lat": 48.8566, "lon": 2.3522 },
          "radius": 10000
        }
      }
    ]
  }
}
Meilisearch:
POST /indexes/my_index/search
{
  "q": "restaurant",
  "filter": "_geoRadius(48.8566, 2.3522, 10000)",
  "sort": ["_geoPoint(48.8566, 2.3522):asc"],
  "hybrid": {
    "semanticRatio": 0.5,
    "embedder": "default"
  }
}
Meilisearch adds geo-distance sorting on top of filtered search — and you still just send a text query instead of a precomputed vector.
The _geo attribute must be added to both filterableAttributes and sortableAttributes.
Qdrant has no equivalent for faceted search. In Meilisearch, you can retrieve value distributions for any filterable attribute: Meilisearch:
POST /indexes/my_index/search
{
  "q": "shoes",
  "facets": ["brand", "color", "size"],
  "hybrid": {
    "semanticRatio": 0.5,
    "embedder": "default"
  }
}
This returns search results along with a count of documents matching each facet value — useful for building filter UIs.

Full-text search (no vectors)

Meilisearch also works as a standalone keyword search engine. If you don’t need semantic search for a particular query, omit the hybrid parameter entirely: Meilisearch:
POST /indexes/my_index/search
{
  "q": "runnign shoes",
  "limit": 10
}
This returns results using keyword matching with automatic typo tolerance (note the typo in “runnign” — Meilisearch handles it). This has no equivalent in Qdrant.

API methods

This section compares Qdrant and Meilisearch API operations.
OperationQdrantMeilisearch
Create collection/indexPUT /collections/{name}POST /indexes
Delete collection/indexDELETE /collections/{name}DELETE /indexes/{index_uid}
Get collection/index infoGET /collections/{name}GET /indexes/{index_uid}
List collections/indexesGET /collectionsGET /indexes
Upsert points/documentsPUT /collections/{name}/pointsPOST /indexes/{index_uid}/documents
Get point/documentGET /collections/{name}/points/{id}GET /indexes/{index_uid}/documents/{id}
Delete points/documentsPOST /collections/{name}/points/deletePOST /indexes/{index_uid}/documents/delete
Scroll/browsePOST /collections/{name}/points/scrollGET /indexes/{index_uid}/documents
SearchPOST /collections/{name}/points/searchPOST /indexes/{index_uid}/search
Multi-searchPOST /collections/{name}/points/search/batchPOST /multi-search
Create payload indexPUT /collections/{name}/indexPATCH /indexes/{index_uid}/settings
Get collection configGET /collections/{name}GET /indexes/{index_uid}/settings
Create snapshotPOST /collections/{name}/snapshotsPOST /snapshots
Health checkGET /healthzGET /health

Front-end components

Qdrant does not include front-end search components. Meilisearch is compatible with Algolia’s InstantSearch libraries through Instant Meilisearch, giving you pre-built widgets for search boxes, hit displays, facet filters, pagination, and more. You can find an up-to-date list of the components supported by Instant Meilisearch in the GitHub project’s README.