Meilisearch Architecture: How It Actually Works
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. The terms:
- Module (or cluster)
- A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
- Modularity (the Q number)
- A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
- Hub
- The most depended-upon symbol inside one module.
- Load-bearing symbols
- PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
- Arrows and their numbers
- How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
- Dependency cycle
- File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
- Mutually recursive symbols
- Functions that call each other, usually the natural shape of parsers and tree-walking code.
Meilisearch is an open-source, Rust-based search engine: a self-hosted alternative to hosted search APIs, built around a fast full-text and vector-ranking core called milli. Symvanta's Louvain community detection organized the codebase into 61 functional modules (modularity Q=0.78, generated by Symvanta's code graph). The largest is the meilisearch server crate itself at 1728 symbols: the actix route handlers, the request and response types they deserialize, and the IndexScheduler handle every handler holds. The product-shaped structures sit inside these clusters as members. IndexScheduler owns the durable, async task queue that every write (document ingestion, settings change, index swap) is enqueued onto and processed from; a Search/SearchContext pair builds and walks a query graph over the index; bucket_sort ranks candidates through an ordered chain of ranking rules; and Embedder wraps one of several pluggable providers (HuggingFace, OpenAI, Ollama, a generic REST backend, user-supplied vectors, or a composite of an index-time and a search-time embedder) for semantic search. The clustering at this commit labels most modules with the generic Rust types that everything routes through (Result, FieldId, Error, IndexUid), so every module name on this page was re-derived from the crates and symbols the cluster actually holds, checked one by one against the graph. The repository also vendors a full typed client for the OpenAI API (external-crates/async-openai) and carries a large integration-test harness (crates/meilisearch/tests/common); both are real graph clusters but neither is Meilisearch's own architecture. Two subsystems are traced end to end on their own pages: the indexing pipeline, how a document becomes searchable, and the ranking rules, how results get ordered.
Module map
The diagram below shows the 10 largest of the 61 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them. The largest is the Meilisearch HTTP API cluster (1728 symbols), the server crate that owns every route. One cluster that would otherwise place fifth by size is left off: the integration-test harness in crates/meilisearch/tests/common (1263 symbols, hub Server), a real graph cluster that is test scaffolding rather than runtime architecture, large only because the tests exercise the whole API surface. The vendored async-openai client stays on the map at 735 symbols, and a dozen smaller vendored fragments (audit-log, fine-tuning, and vector-store types) all trace back to that same bundled crate.
Where to start reading
These are the load-bearing entry points into the codebase: the product-shaped structures at the center of the largest clusters, plus the handful of HTTP routes most requests actually go through. Start here to see how the pieces connect.
IndexSchedulerTaskIndexSearchSearchContextInternedEmbedderSettingsOptperform_federated_searchinsert_objectPOST /indexes/{index_uid}/searchPOST /indexes/{index_uid}/documentsPATCH /indexes/{index_uid}/settingsPOST /multi-searchGET /tasks
The raw PageRank list at this commit is dominated by names the graph groups by bare identifier (new, repr, parse_u32, variant_size) and by helpers the integration-test harness calls from every test (new_shared, default_settings, dynamic_search_rules_server); those are dropped here in favor of the product-shaped structures at the center of each cluster, each one resolved to its defining crate before it went on the list.
A few of these are worth calling out. IndexScheduler is the owner of every asynchronous write; it is declared inside the HTTP API cluster because that is where the handlers hold it, while its own methods (register, tick, create_next_batch, process_batch) cluster with milli's query execution. Task is the enqueued unit of work and the model the /tasks route serves. Index is milli's LMDB environment, the one structure the write path and the read path share. Search and SearchContext carry a query from the HTTP request into ranking, and Interned is the term-id type the ranking rules compare instead of strings. Embedder is the runtime core of the vector-search cluster, one variant per configured provider. Settings is the top-level, per-field-optional configuration object every PATCH /indexes/{index_uid}/settings call updates, and Opt is the process-level counterpart: every CLI flag and environment variable the binary accepts. perform_federated_search is the request-level orchestrator traced in the canonical flow below, and insert_object comes from the flatten-serde-json crate that turns nested JSON documents into the flat field-value pairs the index stores. The five HTTP routes above are Meilisearch's primary surface: search, document writes, settings, multi-index search, and task status.
Key subsystems
Meilisearch HTTP API
Hub IndexUid, 1728 symbols, the largest cluster on the map. The meilisearch server crate: the actix route handlers, the request and response types they deserialize (Param, SearchQuery, IndexUid), ResponseError, and the AuthController that gates every call. IndexScheduler is declared here too, as the shared state the handlers hold. It makes 245 calls into Scheduler Runtime and Query Execution and 244 into Task and Error Types, because nearly every handler ends by enqueuing a task or reading one back.
Scheduler Runtime and Query Execution
Hub Result, 1413 symbols. Louvain grouped the scheduler loop and milli's query execution into one cluster, and its hub is the crate-local Result alias both sides return. It holds tick, register, create_next_batch, process_batch, Queue, and IndexMapper from the index-scheduler crate, next to milli's Search, SearchContext, QueryGraph, execute_search, bucket_sort, and every RankingRule implementation. It calls into Index Storage and Write Pipeline 253 times and into Query Term Interning and Bitmaps 183 times, and takes 447 calls back from the write pipeline. The indexing pipeline trace follows the scheduler half of this cluster from an HTTP write to a committed index; the ranking rules trace follows the query half.
Task and Error Types
Hub Result, 1392 symbols. The meilisearch-types task model: Task, KindWithContent, Kind, Status, TaskId, Network, and the error types the other crates return. This is the shape of a single enqueued job (document addition, settings update, index swap, snapshot) moving from enqueued through processing to succeeded or failed. It makes 191 calls into the HTTP API cluster and 129 into the settings model, since a settings change is itself dispatched as an asynchronous task.
Index Storage and Write Pipeline
Hub Index, 1369 symbols. milli's LMDB Index and the write path that fills it: the index entry point, extract_all, write_to_db, the ExtractorBbqueueSender channel between them, and FilterableAttributesRule. Its 447 calls into Scheduler Runtime and Query Execution are the heaviest edge on the whole map, and its 301 calls into On-disk Codecs and Field Ids are the second heaviest, because extraction is a loop that resolves document fields to field ids and writes their postings.
On-disk Codecs and Field Ids
Hub FieldId (a u16 field identifier), 837 symbols. milli's heed codecs and key types: DocumentId, DelAdd, FacetGroupKey, FacetGroupKeyCodec, BytesRefCodec, OrderedF64Codec, and the LMDB key-size bound. It calls into Index Storage and Write Pipeline 230 times and back into Scheduler Runtime and Query Execution 198 times: reads and writes both flow through field identifiers and the on-disk databases they key.
Embedders and Vector Search
Hub EmbedError, 706 symbols. milli's vector module: Embedder with one variant per configured provider (HuggingFace, OpenAI, Ollama, a generic REST backend, user-provided vectors, and a composite of an index-time and a search-time embedder), plus RuntimeEmbedder, RuntimeFragment, Embedding, DistributionShift, and the EmbedError family. It calls into Index Storage and Write Pipeline 58 times and into the settings model 40 times, because an embedder is configured per index and rebuilt when those settings change.
Vendored code and configuration
Four more clusters round out the top ten. Dump Import and Export (hub Error, 977 symbols) is the dump crate: IndexMetadata, Version, UpdateState, and the per-version readers that import and export a Meilisearch dump. Vendored async-openai Client (hub OpenAIError, 735 symbols) is the bundled external-crates/async-openai typed client. CLI Options and Runtime Features (hub default_settings, 707 symbols) holds Opt, RuntimeTogglableFeatures, and the API-key bootstrap. Index Settings Model (hub Setting, 624 symbols) holds Settings and the Setting<T> three-state wrapper that lets a configurable field be explicitly set, reset, or left alone rather than defaulting silently.
Canonical request flow
The representative flow traced here is a document search: what happens when a client calls POST /indexes/{index_uid}/search.
search_with_post(and itsGETcounterpartsearch_with_url_query) is the actix handler. It takes a search permit from theSearchQueue, builds aDocumentSearcharound the parsed query, and awaits itsexecute. When thelegacy_searchexperimental feature is on it falls back tolegacy_search_with_postinstead, the older path that callsperform_searchdirectly.DocumentSearchis the request bundle the handler hands off. Itsexecutemethod (documents_retrieval/mod.rs:42) checks that the API key authorizes each index, applies any tenant-token search rules to the filter, preprocesses the filters, and then runs each query through the federated search path. A single-index search runs as a one-query federation, so there is one code path for both.perform_federated_searchpartitions the queries into local indexes and remote network shards, runs the local ones throughSearchByIndex::execute(federated/perform.rs:1335), and merges the results by weighted score.SearchByIndex::executeopens the index read transaction and resolves theSearchKindfor the query: keyword only, semantic only, or hybrid.prepare_searchbuilds themilli::Searchfrom that query. It applies the deadline, the ranking-score threshold, the filter, the sort criteria, the distinct field, and any dynamic search rules configured on the index.search_from_kindruns it. Keyword and semantic searches both go throughSearch::execute; a hybrid search callsexecute_hybridwith its semantic ratio, which runs both orderings and blends them.execute_searchis whatSearch::executecalls. It resolves the ranking-rule chain for the query, viaget_ranking_rules_for_query_graph_searchfor a text query orget_ranking_rules_for_placeholder_searchfor a filter-only browse, checks sort criteria and geo parameters, then callsbucket_sort.bucket_sortwalks the ordered chain ofBoxRankingRuleimplementations (words, typo tolerance, proximity, attribute rank, sort, word position, and exactness in the default chain) over the candidate document set, appliesapply_distinct_ruleto drop duplicates by the configured distinct field, and returns aBucketSortOutputcarryingdocids,scores,all_candidates, and adegradedflag for searches that hit the time budget.execute_searchcopies those into thedocuments_idsanddocument_scoresof thePartialSearchResultit returns.compute_facet_distribution_stats, back in the federated path, buildsComputedFacets, and the ranked documents are formatted intoSearchHits for the JSON response. The ranking rules trace walks steps 6 and 7 in detail.
Health signals
Symvanta detected 0 dependency cycles across 61 modules (modularity Q=0.78). 7 sets of mutually recursive symbols were also detected, the largest being filter-parser (7 symbols).
All seven recursive groups sit exactly where a function walks a tree or an expression grammar and calls itself on the pieces it contains. The biggest is the filter-parser crate's recursive-descent parser (parse_expression, parse_and, parse_or, parse_not, parse_primary, parse_foreign, parse_foreign_operator), which is the shape a parser for a boolean filter grammar with nested AND/OR/NOT takes. The rest are JSON and field-map walkers: flatten-serde-json and permissive-json-pointer (two groups) recurse through nested documents, one flattening them for indexing and the other mapping leaf values back out, json_template parses nested template values, fields_ids_map inserts and looks up field ids by name, and milli's extract seeks leaf values through arrays and objects. Zero cycles at a modularity Q of 0.78 means the module boundaries hold: call traffic mostly stays inside a module, and no cluster depends on another in a loop.
For teams weighing a dedicated search engine against embedding search results straight into an existing store, code embeddings vs. code graph covers the same call-graph-vs-vector-similarity tradeoff from the code-search side.
Auto-generated by Symvanta from the public repo meilisearch/meilisearch at commit 577f7af , licensed MIT .
Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).