Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Milvus Ubuntu
GUIDEInstall Guides

How to Install Milvus on Ubuntu 24.04 VPS: Scalable Open-Source Vector Database

32 min read

How to Install Milvus on Ubuntu 24.04 VPS: Scalable Open-Source Vector Database

Vector search powers the modern AI stack. Semantic search, retrieval-augmented generation (RAG), recommendation engines, image similarity, fraud detection, and agent memory all rely on the same primitive: store high-dimensional embedding vectors and retrieve the nearest neighbors to a query in milliseconds. Milvus is the open-source vector database that has become the de facto standard for workloads that start small and need to scale to billions of vectors without re-architecting.

This guide walks you through installing Milvus Standalone on an Ubuntu 24.04 VPS with Docker Compose, defining a production collection schema, building HNSW and IVF_FLAT indexes, running similarity search, combining dense vectors with BM25 lexical search for hybrid retrieval, organizing data with partitions, browsing everything in the Attu web UI, and configuring backups. By the end you will have a production-ready vector database that can back a RAG pipeline, a semantic search API, or any embedding-driven application.

Prefer a turnkey build? Our CloudCore Professional plan gives you the 12 GB of RAM Milvus needs to comfortably serve several million vectors with HNSW indexing for EUR 19.99/month.

Table of Contents

  • What is Milvus?
  • Why Self-Host Milvus Instead of Using Zilliz Cloud?
  • Prerequisites
  • Step 1: Update the System and Install Docker
  • Step 2: Deploy Milvus Standalone with Docker Compose
  • Step 3: Install the pymilvus Client
  • Step 4: Create a Collection with Vector and Scalar Fields
  • Step 5: Build HNSW and IVF_FLAT Indexes
  • Step 6: Insert Data and Run Similarity Search
  • Step 7: Hybrid Search with BM25 (Milvus 2.4+)
  • Step 8: Organize Data with Partitions
  • Step 9: Install the Attu Web UI
  • Step 10: Backups and Disaster Recovery
  • When to Move to Milvus Cluster Mode
  • Performance Tuning
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is Milvus?

    Milvus is an open-source vector database designed from the ground up for similarity search over embedding vectors. It was first released in 2019 by Zilliz, donated to the LF AI & Data Foundation in 2020, and is now a graduated project with contributors from across the AI industry. At its core Milvus solves one problem extremely well: given a query vector, return the k nearest vectors out of a collection that may contain hundreds of millions or billions of entries, in sub-100-millisecond latency.

    Unlike general-purpose databases that bolt on a vector extension (pgvector on PostgreSQL, for example), Milvus was architected around approximate nearest neighbor (ANN) indexing from day one. It supports a full menu of indexes - HNSW (graph-based, best default), IVF_FLAT and IVF_SQ8 (inverted file with optional scalar quantization), IVF_PQ (product quantization for billion-scale), DISKANN (SSD-resident for datasets bigger than RAM), GPU_CAGRA and GPU_IVF_FLAT (CUDA-accelerated) - plus metric types covering L2, inner product, cosine similarity, and Hamming distance.

    Milvus is not just an index. It provides a collection abstraction with strongly-typed fields, partitions for logical data isolation, scalar filtering combined with vector search, time-travel via multi-version concurrency control, dynamic schema fields, sparse vector support for BM25 and SPLADE, and since version 2.5 a built-in BM25 function that replaces the need for a separate text search service like Elasticsearch for many workloads.

    The practical use cases are broad. Engineering teams use Milvus to power RAG pipelines feeding Ollama, vLLM, and OpenAI-compatible LLMs with retrieved document chunks. E-commerce companies use it for visual product search over image embeddings. Music and video platforms use it for audio fingerprinting and recommendation. Security teams use it for malware classification via binary embeddings. Research labs use it for scientific paper discovery via citation and text embeddings. If your application needs "find things that are similar to this," Milvus is the workhorse.

    Why Self-Host Milvus Instead of Using Zilliz Cloud?

    Zilliz Cloud is the commercial managed Milvus service run by the company that created it. It is a fine product, but self-hosting on your own VPS has several concrete advantages, especially for teams early in their journey or with predictable workloads.

    • Flat, predictable cost. Zilliz Cloud bills by compute unit, storage, and egress. A small RAG workload that keeps 2 million vectors loaded can easily run USD 150-400/month on managed services. The same workload fits comfortably on a CloudCore Professional VPS at EUR 19.99/month.
    • Your embeddings never leave your infrastructure. Embeddings reconstruct a significant fraction of the underlying content. If you are indexing customer documents, internal code, legal contracts, or medical records, keeping the vector index on your own server (alongside the LLM and the source documents) simplifies compliance and eliminates one more data-processing agreement.
    • No egress fees. Cloud providers charge to send data out. On your own VPS every byte of query results is free.
    • Full control over configuration. Tune index parameters, enable experimental features, pin to a specific Milvus version, run custom plugins, or downgrade when a new release regresses on your workload. Managed services intentionally restrict these knobs.
    • Co-locate with your LLM. If you are running Ollama or vLLM on the same network for generation, placing Milvus next to it removes network round-trips and slashes end-to-end RAG latency by 20-80 ms per query.
    • Run anywhere. Self-hosted Milvus runs on any Linux VPS, on-prem, on air-gapped networks, in any jurisdiction. Zilliz Cloud is restricted to the regions its operator supports.
    • Open-source license. Milvus is Apache 2.0. You can fork it, audit it, or embed it without commercial restrictions.

    Cost Comparison at 2 Million 768-dim Vectors

    DimensionZilliz Cloud (Serverless)Zilliz Cloud (Dedicated)Self-Hosted Milvus on VPS
    Monthly cost~USD 60-120~USD 300-500EUR 19.99 (CloudCore Professional)
    Vectors loaded2M (768-dim)2M (768-dim)2M (768-dim)
    Query latency20-50 ms10-30 ms15-40 ms (same-VPS)
    Data sovereigntyVendor regionVendor regionYour server
    Egress feesYesYesNone
    Hybrid BM25 searchYesYesYes
    Self-hosting is the right call until a single node cannot hold your working set, at which point the Milvus cluster deployment (covered later) takes over.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • At least 8 GB RAM (16 GB recommended for production and larger indexes)
    • At least 50 GB SSD for Milvus, MinIO object storage, and etcd metadata
    • Docker Engine 24+ and the Docker Compose plugin (installed in Step 1)
    • Python 3.10+ on the server or your workstation to run pymilvus examples
    • A domain name pointed at the VPS if you want to expose Attu or the gRPC endpoint externally
    Recommended Plan: CloudCore Professional
    >
    For Milvus Standalone serving a few million vectors with HNSW, we recommend CloudCore Professional:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 100 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 19.99/month
    >
    This sizes comfortably for 5M+ 768-dimensional embeddings with HNSW, scalar filtering, and an application process on the same box. For billion-scale workloads, step up to the 32 GB and 64 GB plans.

    Connect to your server:

    bash
    ssh root@your-server-ip

    Step 1: Update the System and Install Docker

    Update packages and reboot if a new kernel was installed:

    bash
    sudo apt update && sudo apt upgrade -y

    Install Docker Engine and the Compose plugin from Docker's official repository:

    bash
    sudo apt install -y ca-certificates curl gnupg lsb-release
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    Verify the install:

    bash
    docker --version
    docker compose version

    Expected output:

    text
    Docker version 27.3.1, build ce12230
    Docker Compose version v2.29.7

    Add your user to the docker group so you can run commands without sudo:

    bash
    sudo usermod -aG docker $USER
    newgrp docker

    If you need a more detailed walkthrough of Docker itself, see our Docker install guide.

    Step 2: Deploy Milvus Standalone with Docker Compose

    Milvus Standalone is a single binary that embeds the query, data, and index nodes into one process, but it still depends on two external components: etcd for metadata storage and MinIO (or any S3-compatible object store) for persisting segments. Docker Compose is the cleanest way to run all three.

    Create a working directory:

    bash
    sudo mkdir -p /opt/milvus
    cd /opt/milvus

    Download the official docker-compose.yml file for the latest stable release:

    bash
    sudo curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/deployments/docker/standalone/docker-compose.yml -o docker-compose.yml

    Inspect the file. It provisions three services:

    • etcd (bitnami/etcd image) on port 2379 for metadata
    • minio (minio/minio image) on ports 9000/9001 for object storage
    • standalone (milvusdb/milvus image) on port 19530 for gRPC and 9091 for the HTTP health endpoint
    Start the stack:

    bash
    sudo docker compose up -d

    Expected output (abbreviated):

    text
    [+] Running 4/4
     ✔ Network milvus         Created
     ✔ Container milvus-etcd       Started
     ✔ Container milvus-minio      Started
     ✔ Container milvus-standalone Started

    Wait for the health check to pass (typically 30-60 seconds on a fresh install while etcd initializes):

    bash
    sudo docker compose ps

    Expected output:

    text
    NAME                IMAGE                          STATUS                  PORTS
    milvus-etcd         quay.io/coreos/etcd:v3.5.16    Up 45 seconds (healthy) 2379/tcp, 2380/tcp
    milvus-minio        minio/minio:RELEASE.2024-...   Up 45 seconds (healthy) 9000-9001/tcp
    milvus-standalone   milvusdb/milvus:v2.5.4         Up 30 seconds (healthy) 0.0.0.0:9091->9091/tcp, 0.0.0.0:19530->19530/tcp

    Confirm the Milvus HTTP health endpoint:

    bash
    curl http://localhost:9091/healthz

    Expected output:

    text
    OK

    Milvus is now running. By default it listens on 19530 for gRPC (the protocol pymilvus uses) and 9091 for HTTP metrics and health.

    Bind Milvus to a Private Network

    If your VPS has a private network interface, edit docker-compose.yml and change the standalone service's port mapping from "19530:19530" to "127.0.0.1:19530:19530" or to the private IP. For external access, always expose Milvus through a reverse proxy with TLS and authentication - never expose 19530 directly to the public internet.

    Step 3: Install the pymilvus Client

    Everything you do with Milvus goes through a client library. The official Python client is pymilvus, and it is what we will use throughout this guide. You can install it on the Milvus server itself or on a separate application box.

    Install Python and create a virtualenv:

    bash
    sudo apt install -y python3 python3-pip python3-venv
    python3 -m venv ~/milvus-env
    source ~/milvus-env/bin/activate

    Install pymilvus and a few helpers we will use for embeddings:

    bash
    pip install --upgrade pip
    pip install "pymilvus>=2.5.0" "pymilvus[model]" numpy

    The [model] extra installs sentence-transformers so you can generate real embeddings locally without an external API. Verify the client can connect:

    python
    # test_connection.py
    from pymilvus import MilvusClient

    client = MilvusClient(uri="http://localhost:19530") print("Collections:", client.list_collections()) print("Connected:", client.get_server_version())

    Run it:

    bash
    python test_connection.py

    Expected output:

    text
    Collections: []
    Connected: v2.5.4

    Official clients are also available for Go, Java, Node.js, and a REST gateway. See the Milvus documentation for language-specific install instructions.

    Step 4: Create a Collection with Vector and Scalar Fields

    A collection in Milvus is analogous to a table in a relational database. Each row is an entity with a primary key, one or more vector fields, and any number of scalar fields that you can filter on.

    The example below creates a documents collection suitable for a RAG pipeline. It stores a 384-dimensional embedding (the output of the all-MiniLM-L6-v2 sentence-transformer), plus the source text, a category tag, a timestamp, and a boolean flag.

    python
    # create_collection.py
    from pymilvus import MilvusClient, DataType

    client = MilvusClient(uri="http://localhost:19530")

    COLLECTION = "documents"

    if client.has_collection(COLLECTION): client.drop_collection(COLLECTION)

    schema = client.create_schema( auto_id=True, enable_dynamic_field=True, )

    schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True) schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=384) schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=8192) schema.add_field(field_name="category", datatype=DataType.VARCHAR, max_length=64) schema.add_field(field_name="published_at", datatype=DataType.INT64) schema.add_field(field_name="is_public", datatype=DataType.BOOL)

    client.create_collection( collection_name=COLLECTION, schema=schema, )

    print("Created collection:", COLLECTION) print(client.describe_collection(COLLECTION))

    Run it:

    bash
    python create_collection.py

    Key choices to understand:

    • auto_id=True tells Milvus to generate the primary key automatically. Set it to False if you want to supply your own IDs (for example, to match rows in your source-of-truth database).
    • enable_dynamic_field=True lets you insert additional JSON properties at write time without altering the schema. This is useful when your metadata evolves rapidly.
    • DataType.FLOAT_VECTOR with dim=384 must exactly match the output dimensionality of your embedding model. For OpenAI text-embedding-3-small use 1536; for Cohere embed-v3 use 1024; for nomic-embed-text use 768.
    • VARCHAR with max_length is enforced at insert time. Pick a size that comfortably fits your longest chunk plus headroom.
    Milvus also supports INT8_VECTOR, FLOAT16_VECTOR, BFLOAT16_VECTOR, BINARY_VECTOR, and SPARSE_FLOAT_VECTOR (the last for BM25 and SPLADE-style lexical representations).

    Step 5: Build HNSW and IVF_FLAT Indexes

    An unindexed collection still works - Milvus will do a brute-force scan - but latency scales linearly with dataset size. For anything bigger than a few thousand vectors you need an approximate nearest neighbor (ANN) index.

    HNSW (Recommended Default)

    HNSW (Hierarchical Navigable Small World) is a graph-based index that delivers the best recall-latency trade-off for most workloads. It is memory-hungry but fast.

    python
    # create_index_hnsw.py
    from pymilvus import MilvusClient

    client = MilvusClient(uri="http://localhost:19530") COLLECTION = "documents"

    index_params = client.prepare_index_params() index_params.add_index( field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 200}, )

    client.create_index( collection_name=COLLECTION, index_params=index_params, sync=True, )

    client.load_collection(COLLECTION) print("Index built and collection loaded into memory.")

    Parameter tuning:

    • M is the maximum number of outgoing edges per graph node. Higher M (32, 64) increases recall and memory; lower M (8, 12) is faster to build but less accurate. 16 is the standard default.
    • efConstruction controls build-time graph quality. Range 100-500. Higher means better recall at query time, at the cost of build speed.
    • metric_type must match how you computed your embeddings. Most modern sentence-transformers use COSINE. OpenAI embeddings also use cosine. Use L2 only if your model is explicitly trained for it.
    At query time you tune ef (the search beam width). See Step 6.

    IVF_FLAT (Memory-Efficient Alternative)

    IVF_FLAT partitions vectors into nlist clusters using k-means, then searches the nprobe closest clusters at query time. It uses less RAM than HNSW because it stores the raw vectors rather than a graph, and it is ideal for datasets that change frequently or need exact-within-cluster recall.

    python
    index_params = client.prepare_index_params()
    index_params.add_index(
        field_name="embedding",
        index_type="IVF_FLAT",
        metric_type="COSINE",
        params={"nlist": 1024},
    )
    client.create_index(COLLECTION, index_params, sync=True)

    Rule of thumb for nlist: 4 * sqrt(N) where N is your dataset size. For 1 million vectors that is about 4000.

    Index Comparison

    IndexBuild TimeRAM UsageRecall @ k=10Query LatencyBest For
    FLATInstant1x raw100% (exact)Slow at scale< 100k vectors
    HNSWSlow2x raw98-99%FastestGeneral-purpose default
    IVF_FLATMedium1x raw95-98%FastFrequent updates
    IVF_SQ8Medium0.25x raw90-95%FastMemory-constrained
    IVF_PQSlow0.1x raw85-92%FastBillion-scale
    DISKANNSlow0.1x raw (RAM)95-97%MediumLarger-than-RAM
    For most CloudCore Professional deployments with datasets up to 10 million vectors, HNSW is the right first choice. Revisit only when RAM becomes the binding constraint.

    Step 6: Insert Data and Run Similarity Search

    Now generate some real embeddings and query them.

    Generate Embeddings and Insert

    python
    # insert_data.py
    from pymilvus import MilvusClient
    from pymilvus.model.dense import SentenceTransformerEmbeddingFunction
    import time

    client = MilvusClient(uri="http://localhost:19530") COLLECTION = "documents"

    embedder = SentenceTransformerEmbeddingFunction( model_name="all-MiniLM-L6-v2", device="cpu", )

    docs = [ ("A VPS provides dedicated resources in a virtualized environment.", "hosting"), ("RAG combines retrieval with generative language models.", "ai"), ("Milvus is an open-source vector database for similarity search.", "ai"), ("HNSW graphs enable sub-millisecond nearest neighbor queries.", "ai"), ("Docker Compose orchestrates multi-container applications.", "devops"), ("UFW is a user-friendly front-end for iptables on Ubuntu.", "devops"), ("Cosine similarity measures the angle between two vectors.", "math"), ("Ollama runs large language models locally on CPU or GPU.", "ai"), ]

    embeddings = embedder([d[0] for d in docs])

    rows = [ { "embedding": embeddings[i].tolist(), "text": docs[i][0], "category": docs[i][1], "published_at": int(time.time()), "is_public": True, } for i in range(len(docs)) ]

    result = client.insert(collection_name=COLLECTION, data=rows) print(f"Inserted {result['insert_count']} entities")

    client.flush(collection_name=COLLECTION)

    Run it:

    bash
    python insert_data.py

    Run a Top-k Similarity Search

    python
    # search.py
    from pymilvus import MilvusClient
    from pymilvus.model.dense import SentenceTransformerEmbeddingFunction

    client = MilvusClient(uri="http://localhost:19530") embedder = SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")

    query = "how to run language models on my own server" query_vec = embedder([query])[0].tolist()

    results = client.search( collection_name="documents", data=[query_vec], limit=3, output_fields=["text", "category"], search_params={"metric_type": "COSINE", "params": {"ef": 64}}, )

    for hit in results[0]: print(f"score={hit['distance']:.4f} [{hit['entity']['category']}] {hit['entity']['text']}")

    Expected output:

    text
    score=0.7821  [ai]  Ollama runs large language models locally on CPU or GPU.
    score=0.5103  [ai]  RAG combines retrieval with generative language models.
    score=0.3415  [ai]  Milvus is an open-source vector database for similarity search.

    Filtered Search (Scalar Expression)

    Combine vector search with SQL-like scalar filters using the filter argument:

    python
    results = client.search(
        collection_name="documents",
        data=[query_vec],
        limit=5,
        filter='category == "ai" and is_public == True',
        output_fields=["text", "category", "published_at"],
        search_params={"metric_type": "COSINE", "params": {"ef": 64}},
    )

    Supported expression operators include ==, !=, >, <, >=, <=, in, not in, like, and, or, not. This is what makes Milvus dramatically more useful than a raw ANN library: you get vector ranking and metadata filtering in one round-trip.

    Step 7: Hybrid Search with BM25 (Milvus 2.4+)

    Dense embeddings capture semantics but sometimes miss exact term matches ("MIL-STD-810", a product SKU, a function name). BM25 is the classic lexical scoring function that search engines have used for decades. Milvus 2.4 added native support for sparse vectors, and 2.5 added a built-in BM25 function that automatically tokenizes text and stores sparse term-frequency vectors - giving you true hybrid search in a single database.

    Create a Collection with a BM25 Function

    python
    # hybrid_collection.py
    from pymilvus import MilvusClient, DataType, Function, FunctionType

    client = MilvusClient(uri="http://localhost:19530") COLLECTION = "docs_hybrid"

    if client.has_collection(COLLECTION): client.drop_collection(COLLECTION)

    schema = client.create_schema(auto_id=True, enable_dynamic_field=True) schema.add_field("id", DataType.INT64, is_primary=True) schema.add_field("text", DataType.VARCHAR, max_length=8192, enable_analyzer=True) schema.add_field("dense", DataType.FLOAT_VECTOR, dim=384) schema.add_field("sparse", DataType.SPARSE_FLOAT_VECTOR)

    bm25_fn = Function( name="bm25_fn", input_field_names=["text"], output_field_names=["sparse"], function_type=FunctionType.BM25, ) schema.add_function(bm25_fn)

    client.create_collection(collection_name=COLLECTION, schema=schema)

    Milvus now populates the sparse field automatically whenever you insert a text value. You do not tokenize on the client side.

    Build Indexes for Both Fields

    python
    index_params = client.prepare_index_params()
    index_params.add_index(
        field_name="dense",
        index_type="HNSW",
        metric_type="COSINE",
        params={"M": 16, "efConstruction": 200},
    )
    index_params.add_index(
        field_name="sparse",
        index_type="SPARSE_INVERTED_INDEX",
        metric_type="BM25",
    )
    client.create_index(COLLECTION, index_params, sync=True)
    client.load_collection(COLLECTION)

    Run Hybrid Search with RRF Reranking

    python
    # hybrid_search.py
    from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker
    from pymilvus.model.dense import SentenceTransformerEmbeddingFunction

    client = MilvusClient(uri="http://localhost:19530") embedder = SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")

    query = "install milvus on ubuntu" query_vec = embedder([query])[0].tolist()

    dense_req = AnnSearchRequest( data=[query_vec], anns_field="dense", param={"metric_type": "COSINE", "params": {"ef": 64}}, limit=20, )

    sparse_req = AnnSearchRequest( data=[query], anns_field="sparse", param={"metric_type": "BM25"}, limit=20, )

    results = client.hybrid_search( collection_name="docs_hybrid", reqs=[dense_req, sparse_req], ranker=RRFRanker(k=60), limit=10, output_fields=["text"], )

    for hit in results[0]: print(f"{hit['distance']:.4f} {hit['entity']['text']}")

    RRFRanker uses Reciprocal Rank Fusion, the standard combiner for heterogeneous result lists. For weighted blending use WeightedRanker(0.7, 0.3) to favor dense (semantic) or sparse (lexical) signal. Hybrid search typically improves recall by 5-15% over pure vector search on mixed-intent queries, especially those containing proper nouns, numeric identifiers, or rare terms.

    Step 8: Organize Data with Partitions

    Partitions are logical subdivisions of a collection. All entities share the same schema, but you can target queries at a specific partition, skipping every segment that does not belong to it. This is the right tool for per-tenant isolation, per-category sharding, or time-based data lifecycle.

    python
    # partitions.py
    from pymilvus import MilvusClient

    client = MilvusClient(uri="http://localhost:19530") COLLECTION = "documents"

    Create partitions for multi-tenant data

    for tenant in ["tenant_a", "tenant_b", "tenant_c"]: client.create_partition(COLLECTION, tenant)

    print(client.list_partitions(COLLECTION))

    Insert into a specific partition

    client.insert( collection_name=COLLECTION, partition_name="tenant_a", data=[{ "embedding": [0.1] * 384, "text": "Tenant A private document", "category": "internal", "published_at": 0, "is_public": False, }], )

    Search within a partition (skips all other tenants entirely)

    results = client.search( collection_name=COLLECTION, partition_names=["tenant_a"], data=[[0.1] * 384], limit=5, output_fields=["text"], )

    Partition queries are dramatically faster than filtered queries because they prune at the segment level, not at scan time. The default limit is 1024 partitions per collection, raisable via the maxPartitionNum config. For tens of thousands of tenants use the newer Partition Key feature, which lets you declare a field (for example tenant_id) as the partition key and have Milvus handle routing automatically.

    Step 9: Install the Attu Web UI

    Attu is the official graphical management UI for Milvus. It lets you browse collections, inspect schemas, visualize index stats, run vector queries from the browser, and monitor resource usage without writing any Python.

    Append Attu to your Docker Compose stack:

    bash
    sudo tee -a /opt/milvus/docker-compose.override.yml > /dev/null <<'EOF'
    services:
      attu:
        container_name: milvus-attu
        image: zilliz/attu:v2.5
        environment:
          MILVUS_URL: milvus-standalone:19530
        ports:
          - "127.0.0.1:3000:3000"
        depends_on:
          - standalone
        networks:
          - default
    EOF

    Start Attu:

    bash
    cd /opt/milvus
    sudo docker compose up -d attu

    Attu now listens on 127.0.0.1:3000. Tunnel to it via SSH from your workstation:

    bash
    ssh -L 3000:localhost:3000 root@your-server-ip

    Open http://localhost:3000 in your browser, enter milvus-standalone:19530 as the Milvus address, and click Connect. You will see every collection, partition, and index you have created.

    Expose Attu Publicly (Optional)

    If you want to access Attu without an SSH tunnel, put it behind Nginx with SSL and HTTP basic auth, the same pattern described in our Ollama guide. Never expose Attu directly on the public internet without authentication.

    Step 10: Backups and Disaster Recovery

    The official backup tool is milvus-backup, which snapshots collections (schema + data + index definitions) to an S3-compatible bucket.

    Install milvus-backup

    bash
    wget https://github.com/zilliztech/milvus-backup/releases/latest/download/milvus-backup_Linux_x86_64.tar.gz
    tar -xzf milvus-backup_Linux_x86_64.tar.gz
    sudo mv milvus-backup /usr/local/bin/
    milvus-backup --version

    Configure milvus-backup

    Create /opt/milvus/backups/configs/backup.yaml pointing at your local Milvus and the MinIO bucket the Compose stack already runs:

    yaml
    log:
      level: info
    milvus:
      address: localhost
      port: 19530
    minio:
      address: localhost
      port: 9000
      accessKeyID: minioadmin
      secretAccessKey: minioadmin
      useSSL: false
      bucketName: milvus-backup
      rootPath: backup
    backup:
      maxSegmentGroupSize: 2G
      parallelism:
        backupCollection: 4
        copydata: 128
        restoreCollection: 2

    Create the backup bucket in MinIO (one-time):

    bash
    docker exec milvus-minio mc mb /data/milvus-backup || true

    Create a Backup

    bash
    milvus-backup create --config /opt/milvus/backups/configs/backup.yaml \
      --name daily_$(date +%Y%m%d) \
      --collections documents,docs_hybrid

    Restore from a Backup

    bash
    milvus-backup restore --config /opt/milvus/backups/configs/backup.yaml \
      --name daily_20260416 \
      --suffix _restored

    Off-Site Backups

    To move backups off the VPS, point the MinIO bucket at real S3 (edit the Compose file to use AWS credentials), or add a nightly cron that rclone syncs the MinIO milvus-backup bucket to a second region. Combined with restic snapshots of /opt/milvus/volumes, you have two independent recovery paths.

    Cron for Daily Backups

    bash
    sudo tee /etc/cron.d/milvus-backup > /dev/null <<'EOF'
    0 3   * root milvus-backup create --config /opt/milvus/backups/configs/backup.yaml --name nightly_$(date +\%Y\%m\%d) >> /var/log/milvus-backup.log 2>&1
    EOF

    Keep a retention window of 14-30 nightlies plus weekly and monthly snapshots. Delete old backups with milvus-backup delete --name <backup_name>.

    When to Move to Milvus Cluster Mode

    Milvus Standalone handles most workloads up to the single-node limits of your VPS: typically 50-200 million vectors at 768 dimensions with HNSW, assuming 32-64 GB RAM. You should plan for Milvus Cluster when any of these are true:

    • Working set exceeds a single node's RAM, even with IVF_PQ or DISKANN
    • Ingest throughput exceeds ~50k QPS sustained write
    • You need multi-replica query nodes for high availability (Standalone has no replication)
    • You need separate compute pools for ingest vs query to isolate noisy neighbors
    • You are running across multiple availability zones
    Milvus Cluster is deployed on Kubernetes using the official Milvus Operator or the Helm chart. It breaks the monolith into eight microservices (Proxy, Root/Data/Query/Index Coordinators, Data/Query/Index Nodes) and uses Pulsar or Kafka as the write-ahead log. If you are already running a Kubernetes cluster with k3s, adding Milvus via Helm is a clean operation.

    The upgrade path is smooth: backup your Standalone collections with milvus-backup, restore them into the new Cluster deployment, point your application at the new gRPC endpoint. Keep Standalone running in parallel during the migration if you want zero downtime.

    Performance Tuning

    Segment Size

    Milvus writes data into segments. Smaller segments mean faster queries (less data scanned per segment) but more segments to merge. Tune via dataCoord.segment.maxSize in milvus.yaml (default 512 MB). For write-heavy workloads raise to 1024; for query-heavy keep lower.

    ef at Query Time (HNSW)

    The single most impactful knob for HNSW:

    python
    search_params={"metric_type": "COSINE", "params": {"ef": 128}}
    • ef=32: fast, ~95% recall
    • ef=64: default, ~98% recall
    • ef=128: slower, ~99% recall
    • ef=256: slowest, ~99.5% recall
    Always set ef >= limit.

    nprobe (IVF family)

    python
    search_params={"params": {"nprobe": 32}}

    Higher nprobe = better recall, linear cost. Start at nprobe=16 and tune.

    Load Only the Fields You Need

    When calling load_collection, pass load_fields=["embedding", "text"] to keep less-used scalar columns off-heap.

    Resource Limits in Docker Compose

    Cap the Milvus container to prevent OOM on shared VPS:

    yaml
    services:
      standalone:
        deploy:
          resources:
            limits:
              memory: 8g
            reservations:
              memory: 4g

    Monitor with Prometheus and Grafana

    Milvus exposes Prometheus metrics at http://localhost:9091/metrics. Scrape with Prometheus, visualize in Grafana. Zilliz publishes an official Grafana dashboard (ID 18521).

    Troubleshooting

    ProblemCauseSolution
    milvus-standalone stuck restartingetcd or MinIO not readyCheck docker compose logs etcd minio. Increase health-check timeout in compose file. Ensure /opt/milvus/volumes is writable.
    RPC error: connection refused from pymilvusWrong host/port or service still startingWait 60s after docker compose up. Verify curl http://localhost:9091/healthz returns OK. Check firewall: sudo ufw status.
    ParamError: metric type not matchSchema metric vs index metric mismatchDrop the index and rebuild with the metric you used at insert time (COSINE vs L2 vs IP).
    No memory to load collectionWorking set larger than configured limitsReduce limit in search, remove unused fields from load_fields, switch HNSW to IVF_SQ8 or IVF_PQ, or upsize the VPS.
    Queries very slow at low efIndex not loaded or wrong nprobeCall client.load_collection() after any new index. Tune ef up to 128. Run client.get_load_state() to confirm.
    Attu cannot connect from browserUsing localhost inside DockerIn the Attu connect dialog use milvus-standalone:19530 (the Docker service name), not localhost.
    Disk usage growing indefinitelyOld segments not compactedCall client.compact(collection_name) or set dataCoord.compaction.enableAutoCompaction: true.
    BM25 function not found in hybrid searchMilvus version < 2.5Upgrade: pull the latest milvusdb/milvus:v2.5.x image and docker compose up -d.

    Viewing Logs

    bash
    sudo docker compose logs -f standalone
    sudo docker compose logs -f etcd
    sudo docker compose logs -f minio

    Milvus is verbose at INFO level. To quiet it, set log.level: warn in milvus.yaml.

    FAQ

    How does Milvus compare to Qdrant and Weaviate?

    All three are excellent open-source vector databases with significant overlap. Milvus is the most mature and battle-tested at billion-scale, has the richest index menu (HNSW, IVF, DISKANN, GPU), and the strongest ecosystem for enterprise deployments. Qdrant has a simpler operational story, excellent filtered-search performance, and Rust-native implementation. Weaviate bundles modules for OpenAI, Cohere, and other model providers, making it a "batteries-included" RAG database.

    For the single-VPS self-hosted scenario, pick Milvus when you expect to scale past 10 million vectors or need GPU indexes. Pick Qdrant for operational simplicity at small-to-medium scale. Pick Weaviate if you want built-in vectorization modules so you never write embedding code yourself. All three have excellent Python clients and integrate with LangChain and LlamaIndex.

    Does Milvus require MinIO, or can I use S3?

    Milvus requires any S3-compatible object store. MinIO is the default because it runs in the same Compose file, but you can point Milvus at AWS S3, Cloudflare R2, Backblaze B2, Contabo Object Storage, or Wasabi by editing minio.address, minio.accessKeyID, minio.secretAccessKey, and minio.bucketName in milvus.yaml. Using external object storage is the recommended pattern for production - it decouples storage lifecycle from compute lifecycle and simplifies cluster migrations.

    Can Milvus handle real-time updates?

    Yes. Inserts and deletes are reflected in query results within seconds (default consistency_level="Bounded"). For strict read-after-write guarantees pass consistency_level="Strong" at query time, which forces the query node to wait for the latest WAL entry. Upserts work via the upsert API, which deletes the old entity and inserts the new one in a single call. High update volume can fragment segments; run client.compact() nightly or enable enableAutoCompaction to merge small segments.

    How do I secure Milvus for production?

    Milvus supports RBAC with users, roles, and privileges. Enable authentication in milvus.yaml by setting common.security.authorizationEnabled: true and common.security.tlsMode: 2 for mTLS. Then create a root user and grant a least-privilege service account to your application. Place Milvus behind a private network, expose only 19530 to your application subnet, and use Caddy or Nginx if you need to expose gRPC externally. Never expose MinIO 9000 to the public internet - it is the backing object store.

    What embedding model should I pair with Milvus?

    For English text at small scale, all-MiniLM-L6-v2 (384-dim, ~80 MB) is fast and surprisingly good. For better quality use BAAI/bge-large-en-v1.5 (1024-dim) or intfloat/e5-large-v2 (1024-dim). For multilingual work use BAAI/bge-m3 (1024-dim) or sentence-transformers/paraphrase-multilingual-mpnet-base-v2 (768-dim). For code use jinaai/jina-embeddings-v2-base-code (768-dim). Run the embedder on the same VPS via Ollama with nomic-embed-text (768-dim) for a fully local stack where no data leaves your infrastructure.

    How do I integrate Milvus with LangChain?

    bash
    pip install langchain langchain-milvus
    python
    from langchain_milvus import Milvus
    from langchain_ollama import OllamaEmbeddings

    embeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Milvus( embedding_function=embeddings, connection_args={"uri": "http://localhost:19530"}, collection_name="langchain_docs", )

    vectorstore.add_texts(["Milvus is a vector database.", "Ubuntu is a Linux distribution."]) docs = vectorstore.similarity_search("what is milvus", k=3)

    The same pattern works with LlamaIndex (llama-index-vector-stores-milvus), Haystack (haystack-integrations-document-stores-milvus), and Dify (native Milvus connector in the Model Provider UI).

    Next Steps

    You now have a production-ready Milvus deployment with HNSW and BM25 hybrid indexing, partitions, Attu browser UI, and automated backups. Here is where to take it next:

    • Build a full RAG pipeline - Combine this Milvus instance with Ollama for local LLM inference. Write a LangChain retriever that pulls the top-k chunks from your documents collection and feeds them into a Llama 3 prompt on the same VPS. End-to-end latency under 2 seconds, no data ever leaves your server.
    • Compare against Qdrant and Weaviate - Each vector database has different trade-offs. Our Qdrant install guide walks through the Rust-native alternative, and our Weaviate install guide covers the modules-first approach. Running the same dataset through all three is the fastest way to pick the right tool for your workload.
    • Add observability - Scrape the /metrics endpoint into Prometheus, import the Zilliz Grafana dashboard (ID 18521), and alert on query latency, memory pressure, and segment compaction lag.
    • Enable authentication and mTLS - Before exposing Milvus to any untrusted network, turn on RBAC in milvus.yaml, create service accounts per application, and front the gRPC endpoint with mutual TLS.
    • Graduate to Cluster - When a single node cannot hold your working set, deploy Milvus on Kubernetes via the Milvus Operator. Use milvus-backup to migrate collections with minimal downtime.
    • Read the official docs - The Milvus documentation covers advanced topics like time-travel queries, iterator search for very large k, binary vector indexes for hash-based similarity, and the Python SDK reference.

    Need More Horsepower for Your Vector Stack?
    >
    Milvus scales linearly with RAM. If you are pushing past 10 million vectors or running hybrid BM25 + HNSW over long-context chunks, size up to our higher CloudCore tiers.
    >
    - 12 GB RAM - comfortably serves 2-5M 768-dim vectors with HNSW
    - 24 GB RAM - 10-15M vectors, room for the LLM on the same box
    - 48 GB RAM - 30-50M vectors, production RAG workloads
    - 64+ GB RAM - enterprise-scale Standalone before graduating to Cluster
    >
    Choose Your Plan at vps-server.host - all plans include unmetered bandwidth and NVMe storage.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket