Every RAG application eventually hits the same wall: the embeddings outgrow Postgres, and the search that was instant at a hundred thousand vectors becomes a pile of scan queries at a hundred million. Milvus is the database built for that moment — a high-performance vector database written in Go and C++, distributed and Kubernetes-native, designed to organize and search unstructured data as learned vectors alongside the scalars, strings, and JSON that describe them. The feature list reads like a vector-search textbook: HNSW, IVF, FLAT, SCANN, and DiskANN indexes with quantized variants and mmap, GPU indexing through NVIDIA CAGRA, full-text search with BM25, learned sparse embeddings like SPLADE and BGE-M3 in the same collection as dense vectors, reranking functions tying hybrid searches together, multi-tenancy from database down to partition-key isolation, hot and cold storage tiers, and RBAC with TLS. It scales horizontally by separating compute and storage, so read-heavy and write-heavy workloads grow independently, and it runs in three shapes: Milvus Lite as a pip-installable file-based quickstart, Standalone in one container, and the full distributed cluster.
The codebase reflects the split personality honestly. The Go side is a constellation of microservices — a proxy gateway where every request lands, coordinator processes that own metadata and scheduling, DataNodes and QueryNodes that do the work, streaming nodes hosting write-ahead log shards — all coordinated through an etcd-backed metadata layer and a message queue backbone. The C++ side, called segcore, is where those Go services delegate the hot path: SIMD vector kernels, expression evaluation, and segment management at ten thousand lines for the sealed-segment implementation alone. Between them sits a storage format of column-oriented binlogs on S3-compatible object storage. As always in this series, what follows is an educational tour of published source code.
Milvus at a glance: SDKs hit the proxy, which consults RootCoord and the timestamp oracle before publishing to the log; StreamingNode shards feed DataNode flushes into object storage and QueryNode subscribers, coordinators assign work, and QueryNode executes every search through the C++ segcore.
Reading the overview from left to right:
- Python users arrive through pymilvus, Go users through the new client, both hitting the same gRPC surface.
- The proxy authenticates, rate-limits, and fans every request out.
- RootCoord owns collections, partitions, aliases, and roles.
- The timestamp oracle stamps every write with a global order.
- The message queue is the log backbone all writes flow through.
- StreamingNode hosts the write-ahead log shards.
- DataNode flushes the log into binlogs on object storage.
- QueryNode serves searches through the C++ segcore.
Why You Need This
The first reason is the architecture lesson: Milvus is one of the clearest log-based database designs in open source, and reading it teaches the pattern generally. Writes do not go to a page store — they go to a log. The proxy publishes each insert through the MsgStream abstraction, which runs on a message queue backend, and the log is the database: StreamingNode hosts the write-ahead log for its assigned virtual channels, durability comes from the log first, and everything downstream is a materialized view. DataNode subscribes, converts batches through the DataCodec, and writes column-oriented binlogs to object storage; QueryNodes subscribe to the same log for fresh data while loading sealed segments from storage. Ordering is real, not approximate: the timestamp oracle hands out globally increasing time ticks that every message carries. RootCoord at three and a half thousand lines holds the metadata truth in etcd.
The second reason is the query path, which is a distributed-systems and systems-programming education in one. QueryCoord maintains the coordinator’s view of collections, replicas, and nodes; background checkers detect imbalance and propose tasks that the scheduler dispatches — load, release, balance, move. Inside a QueryNode, the delegator at fifteen hundred lines is the clever part: each virtual channel has one, and it splits every search between growing data straight from the log and sealed data loaded from storage, dispatching to the local worker or remote ones and merging results with timestamps so no duplicate or stale row slips through. The segment manager bridges Go and C++, holding loaded segments in a registry with reference counting, while the ingest flow ingests streaming messages into growing segments asynchronously.
The third reason is the C++ engine, where the actual nanoseconds live. The sealed-segment implementation is ten thousand lines handling chunked column storage, index application, mmap, and dual-mode search and retrieval; the growing-segment implementation adds three and a half thousand more for data still arriving. Queries arrive as serialized plans that the plan builder compiles into executable trees — hybrid expression-plus-vector plans where scalar filtering and similarity search fuse — and the execution driver runs them as data flows. The src directory shows the full toolkit: SIMD bitset operations, expression parsing, clustering indexes, minhash, rescorers, and the indexbuilder package that constructs HNSW, IVF, DiskANN, and sparse indexes off the search path. This is the layer where Milvus’s benchmark numbers actually come from, and it is readable.
The detail view: the access tier with client, proxy, and task machinery, the coordinators with their schedulers and checkers, the data path from MsgStream through WAL, DataNode, and DataCodec to binlogs, the query path from ingest flow through delegator and segments into the C++ engine, and the foundation of etcd and object storage.
Walking the detail diagram, a single search request traces the whole system. The client sends gRPC to the proxy, which instantiates one of the typed tasks — createCollectionTask, searchTask, queryTask — each carrying its own pre-execute validation, execute step, and result shaping; the file is the definitive catalog of every operation Milvus supports. Metadata reads resolve through RootCoord over etcd, the plan fans out to the QueryNodes owning relevant channels, and the delegator routes each piece to the local worker or remote ones before merging. On the write side, inserts flow MsgStream to the WAL, then to DataNode services, through the codec into binlogs on object storage — with DataCoord tracking segment states and planning compaction the whole way. In the engine, the sealed segment and the growing segment compile plans and run them on the driver.
From Install to Your First Vector Search
The fastest path needs no server at all. pip install -U pymilvus gives you the SDK, and pip install "pymilvus[milvus-lite]" bundles the embedded engine: instantiate MilvusClient("milvus_demo.db") with a local filename and you have a persistent vector database in-process. From there it is three calls — create_collection with a dimension, insert with your vectors and metadata, search with a query vector, a limit, and output fields. For the standalone server, run the milvusdb/milvus Docker image and point the client at its URI with credentials; the distributed cluster deploys on Kubernetes via the milvus-operator. Index selection is the one real decision: HNSW for latency-critical recall, DiskANN when indexes must live mostly on disk, autoindex to let Milvus choose, GPU CAGRA when you have the cards. Full-text search arrives by defining a BM25 function on a varchar field; hybrid search chains a dense and a sparse request through a reranker. Milvus Lite covers development, Standalone covers most production, and the cluster is there when one machine genuinely is not.
Honest limits: Milvus’s power is also its weight. A distributed deployment is a real distributed system — etcd, message queue, coordinators, multiple node types — with the operational surface that implies, so teams without Kubernetes experience should start with Standalone or Lite. Memory management is a genuine discipline: loading a collection pins segments, and unconfigured memory limits are how newcomers discover OOM kills. The Go and C++ split, while clean at the API boundary, means performance debugging can cross languages. Some advanced features — partition-key multi-tenancy at millions of tenants, serverless cold tiers — behave differently in the self-hosted editions than the docs’ managed-service framing suggests. And the project moves fast enough that internal APIs this tour cites will keep shifting, though the component architecture has been stable for years. For what it aims at — billions of vectors, thousands of queries per second, real-time freshness — it is the most complete open-source answer available. Enjoyed this post? Never miss out on future posts by following us