Architecture meilisearch/meilisearch
Meilisearch Indexing Pipeline, Write to Index
How to read this page
Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. This page is a deep dive into one subsystem of Meilisearch Architecture: How It Actually Works; the hub page holds the whole-repo module map and glossary.
Every write in Meilisearch (adding documents, updating settings, swapping indexes) is asynchronous. A POST /indexes/{index_uid}/documents call does not touch the search index inline: it streams the request body to a file on disk, appends one durable task to a queue, and returns a task id. A background scheduler picks the task up later, groups it with compatible neighbors, and runs the milli update pipeline that actually rewrites the on-disk databases. That split is what lets Meilisearch absorb bursts of writes without blocking search, and IndexScheduler is the structure both halves meet at: the HTTP layer holds it as shared state, and the scheduler loop drives it. This page traces one document from the HTTP handler to a committed index, using the module graph generated by Symvanta.
The pipeline has two halves that never share a thread. The front half is cheap and synchronous with the request: validate, persist the payload, enqueue. The back half runs inside the scheduler's own loop: it opens a write transaction against LMDB, extracts field and word data from the changed documents, and commits roaring-bitmap postings. Everything the search side later reads (the ranking rules trace covers that side) is written here.
The moving parts
document_additionregistertickcreate_next_batchautobatchapply_index_operationindexextract_allwrite_to_dbCboRoaringBitmapCodec
document_addition is the actix handler behind both POST (replace) and PUT (update) on the documents route: it validates the content type, streams the body to an update file, and hands off. register is the queue's front door: it turns the request into a Task (a KindWithContent describing the operation) and persists it in enqueued state. tick is the scheduler loop, create_next_batch and autobatch decide how many enqueued tasks to fold into one transaction, and apply_index_operation is the bridge from a scheduler batch into milli. The index function is milli's update entry point; it fans out into extract_all (which turns documents into word and facet deltas) and write_to_db (which drains those deltas into LMDB). The postings themselves are encoded by CboRoaringBitmapCodec, the compressed-bitmap codec that stores which document ids match each term or facet value. Index (the LMDB environment) and FieldsIdsMap (the string-to-u16 field map) are the two on-disk structures both halves of the pipeline share.
How it works
Here is the canonical path for a document write, from the HTTP handler through the enqueued task to a committed index.
document_additionis the shared handler forPOST /indexes/{index_uid}/documents(replace) and itsPUTsibling (update). It checks the content type, then streams the raw request body to an on-disk update file viacopy_body_to_fileandcreate_update_filerather than parsing every document inline, so the request thread does almost no work.- The
registerfamily appends the write to the queue. The handler builds aKindWithContent::DocumentAdditionOrUpdate(index uid, primary key, method, and the update-file uuid) and passes it toregister_with_custom_metadata(documents.rs:1633), the sibling that also carries the request's custom metadata; both it andregisterare thin wrappers overregister_with_custom_metadata_and_network, which persists theTaskinenqueuedstate and returns its id immediately. The HTTP response is sent here, long before any indexing happens. tickis the scheduler loop, running on its own thread. On each turn it opens a read transaction, askscreate_next_batchfor the next unit of work, processes it, and writes the resulting task states back to the queue.create_next_batchpicks the next unit of work, and for ordinary document writes it delegates tocreate_next_batch_unprioritized, which is what runsautobatchover the enqueued tasks. Consecutive document operations on the same index coalesce into a singleIndexOperation, so a burst ofPOSTcalls is applied in one write transaction instead of one per request.apply_index_operationis reached fromprocess_batch(call site line 183). For a document operation it opens the index write transaction, reads back the currentFieldsIdsMap, builds a milliIndexOperationsfrom the batched update files, and turns it into theDocumentChangesthe indexer consumes.indexis milli's update entry point. It takes thoseDocumentChanges, spins up the extractor thread pool and a bounded bbqueue channel, and runs extraction and writing in parallel: producers extract, a consumer writes, so a large payload never has to fit in memory at once.extract_allruns the extractors over the changed documents, producing the field distribution, aWordDelta(words added, modified, and deleted), and facet-field deltas, and streams the resulting key-value writes into the channel.write_to_dbdrains that channel on a dedicated writer thread and puts each buffer straight into its LMDB database. TheCboRoaringBitmapCodecencoding already happened on the producer side, in the extractor caches and the merger that filled the channel.post_processthen rebuilds the prefix and word-FST databases from the deltas, and the write transaction commits: the document is now searchable.
Where it connects
The scheduler and the query engine share one cluster on the Meilisearch module map: Scheduler Runtime and Query Execution (hub Result, 1413 symbols), which holds tick, register, create_next_batch, and process_batch next to milli's execute_search and bucket_sort. It calls into the Index Storage and Write Pipeline cluster 253 times and into On-disk Codecs and Field Ids 128 times, because every task it runs ends by reading and writing index state through field identifiers. The indexer half lives in Index Storage and Write Pipeline (hub Index, 1369 symbols), where milli's index, extract_all, and write_to_db sit beside the LMDB Index they commit into. Its 447 calls back into the scheduler cluster are the heaviest edge on the whole map, and its 301 calls into On-disk Codecs and Field Ids are the second heaviest: extraction is a loop that resolves document fields to field ids and writes their postings. CboRoaringBitmapCodec itself sits one cluster over, in Query Term Interning and Bitmaps (hub Interned, 591 symbols), alongside the interner and bitmap types the query path reads.
The output of this pipeline is exactly the input to the query side. extract_all writes the word and facet postings; the ranking rules trace shows bucket_sort walking those same postings to order results. The one structure both halves share is Index: the write path commits into it under a write transaction, and every search opens a read transaction against it, which is why the scheduler exposes its own read_txn helper next to the index-level one.
By the numbers
The Scheduler Runtime and Query Execution cluster is 1413 symbols, the second largest of Meilisearch's 61 detected modules, behind the Meilisearch HTTP API cluster at 1728. The milli indexer subtree (crates/milli/src/update/new/indexer) is 15 files and 327 symbols. The two functions where a batched write becomes committed on-disk state are large: apply_index_operation spans lines 45 through 581, and milli's index spans lines 68 through 246. The whole pipeline sits in a graph with zero dependency cycles (modularity Q=0.78), so the write path and the read path stay cleanly separated even though they meet at the same LMDB Index.
Auto-generated by Symvanta from the public repo meilisearch/meilisearch at commit 577f7af , licensed MIT .