Skip to main content
MongoDB is a great home for your documents: flexible schema, easy writes, horizontal scale. What it is not is a search engine. Its text search is coarse, has no typo tolerance, and slows down exactly when you need it most. Meilisearch gives you instant, typo-tolerant, relevance-ranked search, provided your documents actually make it in, and stay current. This guide builds that bridge with Kestra: first a one-shot backfill of a collection, then a scheduled incremental sync that keeps Meilisearch in lock-step with MongoDB as documents are inserted, updated, and deleted, all in declarative YAML.

Why orchestrate the sync

Keeping a search index in sync is a workflow, not a one-liner: it needs to run on a schedule, retry on failure, skip nothing when a run is delayed, and be observable when something breaks. Kestra gives you all of that declaratively. You describe the extract-and-index steps, and Kestra handles scheduling, state, retries, and logging.

Prerequisites

A running Kestra with the Meilisearch and MongoDB plugins, plus a Meilisearch Cloud project. Only Kestra needs to run locally, since Meilisearch is managed:
Install the plugins:
Get your Cloud credentials. In the Meilisearch Cloud dashboard, create a project and copy its Project URL (the url in the flows below) and its Default Admin API Key (Settings, then API Keys). Indexing needs write access. Kestra reads secrets from SECRET_-prefixed, base64-encoded environment variables, referenced as {{ secret('MEILISEARCH_API_KEY') }}.
This guide syncs a books collection in a catalog database. One detail matters up front: use string or numeric _id values. Meilisearch accepts MongoDB’s _id as its primary key, but only when it’s a string or number. A raw ObjectId object won’t do. Storing books with ids like "book-1" keeps things clean:
Note updatedAt and deletedAt stored as ISO-8601 strings. That choice pays off in the incremental step: string timestamps compare correctly with $gte, stay JSON-serializable through the pipeline, and spare you any BSON extended-JSON wrangling in your flow.

Step 1: The first load (backfill)

The MongoDB plugin’s Find task, with store: true, writes matching documents to Kestra’s internal storage as an ION file, precisely the format the Meilisearch DocumentAdd task consumes. Two tasks, no glue:
The projection strips the housekeeping fields so your search documents stay clean. updatedAt and deletedAt are for the sync machinery, not for your search results. DocumentAdd batches the documents and waits for indexing to complete, so the run fails loudly if Meilisearch rejects anything. Run it once and the collection is fully searchable.

Step 2: Incremental sync (the real use case)

Re-indexing the whole collection on every run doesn’t scale. Instead, sync only what changed since the last run, which is why updatedAt is stored on every document. Make sure your application updates it on every write (or use a MongoDB change-tracking mechanism to maintain it). The sync flow runs on a schedule and selects only documents whose updatedAt falls inside a lookback window:
The threshold variable computes an ISO-8601 timestamp ten minutes in the past, and the Find filter uses a plain Mongo $gte query against it. Two properties keep this safe:
  • The lookback (10 min) exceeds the schedule interval (5 min), so overlapping windows guarantee no change is ever missed, even across a delayed or retried run.
  • Overlap is harmless because DocumentAdd is add-or-replace. Re-indexing a document Meilisearch already has just overwrites it by _id. Idempotent by construction.

Handling deletes

DocumentAdd only ever adds or replaces; it never removes. A book deleted in MongoDB would otherwise haunt your search results forever. The fix is soft deletes: your application sets deletedAt to a timestamp instead of removing the document, and the sync flow removes recently-deleted ids from Meilisearch. Add these tasks to the mongodb_incremental_sync flow so each run handles both upserts and deletes:
Two practical notes:
  • Kestra’s jq filter returns a list of results, so jq('map(._id)') yields [["book-6"]]; | first | toJson unwraps it to the ["book-6"] array Meilisearch’s delete-batch endpoint expects.
  • The deletion is asynchronous (the API returns 202 Accepted); the document is gone a moment later.
Inserts and updates flow through upsert_documents, deletes through apply_deletes, and your index stays perfectly consistent with the collection.

Going to production

  • What about Change Streams? MongoDB change streams give true real-time change capture, but they require a replica set and aren’t exposed by the Kestra MongoDB plugin. The scheduled-lookback pattern here is the practical, dependency-free answer for the vast majority of cases. If you truly need real-time, stream changes into Kafka and consume them. See Connect Kafka to Meilisearch with Kestra.
  • Exact watermarks. For minimal re-reads, persist the last-synced timestamp in Kestra’s KV store and filter with $gt against it instead of a fixed window.
  • Retries. Add a retry block to each task so transient MongoDB or network errors self-heal.
  • Backfill once, then sync. Seed the index with the Step 1 flow, then let the schedule run. Idempotent upserts make an accidental re-backfill a no-op.

Wrap-up

With two YAML flows, MongoDB and Meilisearch stay in sync: a backfill for the initial load, and a scheduled incremental sync that handles inserts, updates, and deletes idempotently, powered by ISO-string timestamps, soft deletes, and Meilisearch’s add-or-replace semantics. Keep writing documents to MongoDB, and let Kestra keep search current.