Architecture meilisearch/meilisearch
Meilisearch Indexing Pipeline: From an HTTP Write to a Committed LMDB 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 the Codebase 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 it is why the IndexScheduler is the single most depended-upon structure in the codebase. 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 deep dive 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.registerappends the write to the queue. The handler builds aKindWithContent::DocumentAdditionOrUpdate(index uid, primary key, method, and the update-file uuid), andregisterpersists it as aTaskinenqueuedstate 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_batchgroups compatible enqueued tasks into oneBatchusingautobatch. 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 179). 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 commits the postings into LMDB, encoding each document set withCboRoaringBitmapCodec.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 cluster (Task Management System, hub Result) is the most depended-upon module on the Meilisearch module map: it calls into the request and error plumbing 387 times and into the on-disk FieldId cluster 305 times, because every task it runs ends by reading and writing index state through field identifiers. The indexer half lives in the Indexing and Retrieval cluster (hub FieldId, 1803 symbols), whose CboRoaringBitmapCodec and Index are the on-disk data model the query path later reads from. The heaviest single edge on the whole map, 731 calls from the extraction cluster into FieldId, is this pipeline: extraction is a loop that resolves document fields to field ids and writes their postings.
The output of this pipeline is exactly the input to the query side. extract_all writes the word and facet postings; the ranking rules deep dive 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 Task Management System cluster is 1869 symbols, the largest of Meilisearch's 52 detected modules. The milli indexer subtree (crates/milli/src/update/new/indexer) is 15 files and 328 symbols. The two functions where a batched write becomes committed on-disk state are large: apply_index_operation spans lines 43 through 574, and milli's index spans lines 68 through 246. The whole pipeline sits in a graph with zero dependency cycles (modularity Q=0.77), 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 df81ba4 , licensed MIT .