VectorOpsReport
Pink isometric server stack and a spiked node cluster linked by glowing pink lines and hexagons on a dark blue dotted grid, evoking distributed vector database nodes.
Comparison

Qdrant vs Weaviate vs Milvus: Recall, RAM, and Scale

This comparison covers filtered recall, memory and quantization options, cluster design, multi-tenancy, and scaling across the three engines.

By VectorOpsReport Editorial · ·Updated · 8 min read

Pick Qdrant over Milvus for filtered vector search on one to a few nodes: it is a single Rust binary with filter-aware HNSW. Pick Milvus past 100 million vectors, where its Kubernetes-native distributed mode, DiskANN and GPU search indexes justify running etcd and object storage alongside it. Weaviate wins on built-in vectorization, BM25 hybrid search and large tenant counts.

The choice usually surfaces when a tenant_id filter drops recall@10 and blows up p99 by shifting RAG retrieval from graph search to brute force. All three open source HNSW engines can look identical on vendor QPS charts. They differ in filtering, RAM cost, coordination beyond one node, and tenant limits. Sources are vendor documentation; vendor benchmarks are labeled.

What each one is, mechanically

Qdrant is a single Apache-2.0 Rust binary, v1.19.2. Its only dense index, HNSW, defaults to m: 16, ef_construct: 100. The filterable HNSW index adds graph edges from payload indexes (keyword, integer, float, bool, geo, datetime, text, uuid) to filter during traversal. Since v1.16.0, ACORN-style search explores neighbours-of-neighbours when direct ones are filtered. Vectors are always stored on disk; since v1.19.0 a cold, cached or pinned memory strategy is set per collection component.

Weaviate is BSD-3-Clause Go, v1.40.0, with patch releases still shipping on the 1.39 and 1.38 lines. The vector index reference lists HNSW (defaults efConstruction: 128, maxConnections: 32, dynamic ef between 100 and 500), flat, dynamic (flat to HNSW after 10,000 objects), and HFresh for memory-constrained nodes. Model provider integrations connect it to more than two dozen providers, including OpenAI, Cohere, Hugging Face and Ollama, so objects can be vectorized server-side on insert.

Milvus is Apache-2.0 Go and C++. v3.0.2, released on 20 September 2026, is the current patch of the 3.0 line that opened on 29 July 2026; the maintained 2.6 line reached 2.6.24 on 16 September. Rollback from 3.0 to 2.6 is supported only until you enable a feature that changes the serialized data format, such as Storage V3. Disaggregated components are stateless proxies, one coordinator, streaming, query and data nodes, etcd, MinIO or S3, and the Woodpecker write-ahead log. The widest index menu includes FLAT, IVF_FLAT, IVF_SQ8, IVF_PQ, HNSW, HNSW_SQ, HNSW_PQ, HNSW_PRQ, SCANN, DiskANN, and four GPU indexes, including GPU_CAGRA. The CAGRA paper reports 33 to 77x HNSW’s large-batch throughput at 90 to 95% recall. HNSW defaults are M: 30 and efConstruction: 360.

Qdrant vs Milvus, head to head

Most Milvus vs Qdrant evaluations are settled by operations before algorithms. Both are Apache-2.0 and both offer HNSW, but Qdrant runs as one container while server-mode Milvus starts at three, and Milvus carries index families and GPU search that Qdrant does not. Every row was checked against vendor documentation in October 2026.

QdrantMilvus
Current releasev1.19.2, 5 October 2026 (releases)v3.0.2, 20 September 2026 (release notes)
Smallest installone container; REST on 6333, gRPC on 6334, web UI at /dashboard (quickstart)Milvus Lite inside pymilvus, FLAT index only, Ubuntu and macOS (Lite); Standalone runs milvus-standalone, milvus-etcd and milvus-minio, serving on 19530 with a WebUI on 9091 (Compose guide)
Data modelany JSON payload, no schema (payload); point IDs are 64-bit unsigned integers or UUIDs (points)schema designed before collection creation, INT64 or VARCHAR primary key (schema); undeclared keys land in the $meta JSON field when enable_dynamic_field is on (dynamic field)
Vector typesfloat32, float16, uint8, turbo4 (v1.19.0); sparse; multivectors with the max_sim comparator for ColBERT-style models, capped at number_of_sub_vectors * vector_size < 1,048,576 (vectors)FLOAT, FLOAT16, BFLOAT16, INT8 (HNSW only), BINARY and SPARSE_FLOAT vectors; max dimension 32,768 (limits)
Dense indexesHNSW onlyFLAT, IVF family, HNSW variants, SCANN, DiskANN, GPU indexes; 3.0 adds a FAISS type that takes index-factory strings via faiss_index_name
GPUvector index building since v1.13.0, NVIDIA and AMD images (GPU guide)GPU indexes serve search; 3.0 GPU images moved to CUDA 12.9
Data beyond RAMvectors always on disk, per-component memory strategy, turbo4 storage at about one-eighth of float32mmap per field or index, off by default and not available for DiskANN or GPU indexes (mmap); DiskANN on NVMe
ReshardingQdrant Cloud only, since v1.13.0; self-hosted fixes shard_number at creationup to 16 shards per collection (limits)
Managed free tierQdrant Cloud: 1 node, 0.5 vCPU, 1 GB RAM, 4 GB disk, about 1M 768-dimension vectors, no card; suspended after 1 week idle, deleted after 4 (Cloud docs)Zilliz Cloud Free: 5 GB storage (about 1M 768-dimension vectors), 2.5M vCUs a month, up to 5 collections (Zilliz docs)

Three rows decide most trials. The first is consistency, which the table cannot show: Milvus defaults to Bounded staleness, so a search issued right after an insert can miss the new rows. Strong can be requested per search or query at a latency cost. Score recall only after the golden set is fully visible, or Milvus will look worse than its index is.

The second is footprint. Milvus Standalone’s Docker requirements ask for at least 4 cores and 8 GB of RAM (16 GB recommended), a CPU with SSE4.2, AVX, AVX2 or AVX-512, and etcd on a disk with more than 500 IOPS and p99 fsync under 10 ms. Qdrant’s quickstart is one docker run with a mounted qdrant_storage volume.

The third is growth. On self-hosted Qdrant, resharding is a Cloud feature, so an undersized shard_number means building a new collection. When the corpus outgrows RAM, Qdrant leans on quantization and its cold tier; Milvus adds DiskANN and GPU search indexes, which Qdrant does not offer.

Both published head-to-heads are vendor-run. Qdrant’s benchmark, last updated in 2024, calls Milvus “the fastest when it comes to indexing time” but “not on-par with others when it comes to RPS or latency” with higher-dimension embeddings or more vectors. That run predates Milvus 3.0, whose new index versions are opt-in (dataCoord.targetVecIndexVersion set to 10), so treat it as a hypothesis to retest, not a verdict.

Moving data between Qdrant and Milvus

Each vendor ships a one-way importer into its own engine, so a trial on real data does not need custom ETL. From Milvus to Qdrant, using qdrant/migration:

  1. Create the Qdrant collection yourself, with every payload index your filters need (including is_tenant: true on the tenant key), before data arrives. The tool otherwise creates the collection itself (--migration.create-collection defaults to true), and it cannot know which fields you filter on.
  2. Match the distance metric. --qdrant.distance-metric maps vector names to metrics and defaults to cosine, so set it explicitly if the Milvus field used IP or L2; cosine and dot product rank differently on unnormalized vectors.
  3. Check primary keys on a sample. Qdrant accepts only unsigned 64-bit integers or UUIDs, while Milvus allows signed INT64 and arbitrary VARCHAR keys, and the tool’s README documents no ID mapping for its Milvus source.
  4. Run the importer against Qdrant’s gRPC port:
docker run --net=host --rm -it registry.cloud.qdrant.io/library/qdrant-migration milvus \
    --milvus.url 'http://milvus:19530' \
    --milvus.collection 'docs' \
    --qdrant.url 'http://qdrant:6334' \
    --qdrant.collection 'docs' \
    --migration.batch-size 64
  1. If it stops, rerun the same command. Progress is stored in a _migration_offsets collection; --migration.restart discards it and starts over.
  2. Run the golden-set harness below against both engines with identical filters, and compare filtered recall@10 and p99 before moving traffic. The recall@k benchmarking guide covers building the exact ground truth.

The reverse path is Zilliz’s VTS, built on Apache SeaTunnel. It reads from Qdrant, Pinecone, Weaviate, Elasticsearch, Postgres vector and others, writes to Milvus, and runs from a config file: start from its qdrant.conf example and launch with ./bin/seatunnel.sh --config ./migration.conf -m local.

The metric that matters

Use filtered QPS at target recall with RAM cost, not unfiltered QPS.

Define it on a golden set of N production queries with exact ground truth from a FLAT scan:

recall@k = mean over queries of |approx_topk ∩ exact_topk| / k
QPS@r    = sustained throughput at the ef where recall@k >= r

Vendor charts show unfiltered QPS; filters cause divergence. Weaviate pre-filters through its inverted index into an allow-list, walks HNSW with ACORN (default filterStrategy since v1.34), and uses brute force below flatSearchCutoff (default 40,000). Milvus restricts the search scope to matching entities before search, with iterative mode for complex expressions. Qdrant traverses its filter-aware graph and scans fully below full_scan_threshold. The ACORN paper reports 2 to 1,000x throughput at fixed recall over prior filtered-search approaches. Measure selectivity: a 1% tenant filter differs from a 40% region filter. See HNSW vs IVF tradeoffs for index-family differences and vector search fundamentals for how ANN recall is defined.

Where they actually differ

ConcernQdrantWeaviateMilvus
Compressionscalar 4x, binary up to 32x, product up to 64x, TurboQuant up to 32x (docs)PQ (trained), SQ 4x, BQ 32x, RQ 8/4/1-bit (docs)IVF_SQ8, IVF_PQ, HNSW_SQ/PQ/PRQ, DiskANN on NVMe
Beyond RAMcold memory tier for vectors, index and payloadHFresh; tenant offload to S3DiskANN; query nodes page segments from object storage
ClusterRaft for topology; shard_number defaults to node count; replication_factor default 1 (docs)Raft for metadata, leaderless data replication with ONE/QUORUM/ALL (docs)Coordinator plus stateless workers on Kubernetes; etcd, object store, Woodpecker
Multi-tenancyone collection, is_tenant: true payload index; Cloud caps 1,000 collections (docs)one shard per tenant; docs cite ~170k active tenants on 9 n1-standard-8 nodes, bounded by the open-file limit (docs)database (64 default), collection (65,536 default), partition (1,024 per collection) or partition key routing to 16 partitions (docs)
Hybridsparse vectors, prefetch with RRF (v1.10) and DBSF (v1.11)BM25 plus vector, alpha default 0.75, relativeScoreFusion default since v1.24built-in BM25 function to SPARSE_FLOAT_VECTOR; 3.0 adds Block-Max WAND

Self-hosted Qdrant ignores post-creation replication_factor changes; add shard replicas manually. Qdrant Cloud reconciles automatically. The Milvus deployment guide defines Lite up to a few million vectors, Standalone up to 100 million, and Distributed from 100 million to tens of billions. See vector database memory sizing for RAM.

Wiring it up

One golden-set harness covers all three. An exact scan supplies ground truth for filtered recall@10 and p99.

import time, numpy as np
from qdrant_client import QdrantClient, models as qm
import weaviate
from weaviate.classes.query import Filter
from pymilvus import MilvusClient

queries = np.load("golden_queries.npy")          # (N, dim)
exact = np.load("golden_exact_top10.npy")        # (N, 10) ids from a FLAT scan
tenants = np.load("golden_tenants.npy")          # (N,) tenant per query
K = 10

def qdrant_search(c, q, t):
    r = c.query_points("docs", query=q.tolist(), limit=K,
        query_filter=qm.Filter(must=[qm.FieldCondition(
            key="tenant", match=qm.MatchValue(value=str(t)))]),
        search_params=qm.SearchParams(hnsw_ef=128))
    return [p.id for p in r.points]

def weaviate_search(coll, q, t):
    r = coll.query.near_vector(near_vector=q.tolist(), limit=K,
        filters=Filter.by_property("tenant").equal(str(t)))
    return [o.properties["doc_id"] for o in r.objects]

def milvus_search(c, q, t):
    r = c.search("docs", data=[q.tolist()], limit=K,
        filter=f'tenant == "{t}"', search_params={"params": {"ef": 128}})
    return [hit["id"] for hit in r[0]]

def evaluate(name, fn):
    hits, lat = [], []
    for q, gt, t in zip(queries, exact, tenants):
        t0 = time.perf_counter()
        ids = fn(q, t)
        lat.append(time.perf_counter() - t0)
        hits.append(len(set(ids) & set(gt.tolist())) / K)
    print(f"{name}: recall@{K}={np.mean(hits):.4f} "
          f"p50={np.percentile(lat,50)*1e3:.1f}ms p99={np.percentile(lat,99)*1e3:.1f}ms")

qc = QdrantClient("http://qdrant:6333")
wc = weaviate.connect_to_local(host="weaviate")
mc = MilvusClient("http://milvus:19530")
evaluate("qdrant",   lambda q, t: qdrant_search(qc, q, t))
evaluate("weaviate", lambda q, t: weaviate_search(wc.collections.get("Docs"), q, t))
evaluate("milvus",   lambda q, t: milvus_search(mc, q, t))

Sweep ef (Qdrant hnsw_ef, Weaviate ef, Milvus ef) from 32 to 512. Load-test the lowest value meeting target, not the default; the ef_search explainer covers why recall flattens while latency keeps climbing. Weaviate’s docs note diminishing recall gains above 512. On Milvus, run the sweep after ingest has settled or request Strong consistency, for the staleness reason above.

What you’ll see

Healthy recall-vs-ef curves flatten above target at similar ef, p99 rises roughly linearly with ef, and filtered and unfiltered curves stay close at every served selectivity.

An order-of-magnitude filtered p99 jump at one selectivity marks a brute-force cliff: Weaviate’s flatSearchCutoff, Qdrant’s full_scan_threshold, or a Milvus expression too complex for the standard path. Recall holding unfiltered but falling under a tight filter means a disconnected tenant graph; on Qdrant, a payload index created after ingestion usually omitted filter-aware edges. Week-over-week recall decay without config changes indicates embedding-model or corpus drift, covered in sentryml’s metrics taxonomy, not an engine issue. See low vector search recall: causes and fixes for triage order.

Caveats

Every cited head-to-head is a vendor benchmark. Qdrant’s benchmark page tests Qdrant, Weaviate, Milvus, Elasticsearch and Redis on an 8 vCPU, 32 GiB Azure D8s v3, caps each at 25 GB, covers dbpedia-openai-1M (1536d), deep-image-96 (10M), gist-960 and glove-100, and was last updated in 2024. VectorDBBench, sponsored by Milvus developer Zilliz, covers more than 30 engines across Cohere, OpenAI 500K and 5M, and LAION 100M, reporting QPS, recall, p99 and filtered-search cases. The only independent suite containing all three, ANN-Benchmarks, lists Qdrant and Weaviate as separate entries and represents Milvus via Knowhere, among 38 entries in total, but tests single-query algorithms, not clustered load.

Quantization numbers are ceilings. Qdrant’s 40x binary-quantization speedup and Weaviate’s 98 to 99% 8-bit RQ recall are vendor results for specific embedding models. Below 8-bit requires rescore with oversampling on Qdrant or rescoring on Weaviate, reading full, uncompressed vectors from storage. GPU indexes target throughput, not latency: Milvus’s documentation says a GPU index “may not necessarily reduce latency compared to using a CPU index,” paying off under high request pressure or large query batches. GPU_CAGRA uses roughly 1.8x the raw vector data’s GPU-memory footprint.

Benchmarks omit tenant cardinality. Qdrant and Milvus vendors warn that one collection per tenant is expensive. Weaviate’s shard-per-tenant model scales further but remains bounded by the process open-file limit; offloaded tenants require S3. Qdrant’s shard_number cannot change without collection recreation and should be a multiple of expected node count. Retrieved chunks remain untrusted input and can carry indirect prompt injection into the model’s context.

Which one

Choose Qdrant for filtered search on one node or a handful with minimal operations; Weaviate for server-side embedding, out-of-box BM25 hybrid search, or tens of thousands of isolated tenants; Milvus past 100 million vectors, with Kubernetes and object storage, or for DiskANN or GPU indexes. Otherwise, use the harness and how to choose a vector database. For Qdrant and Milvus set against managed Pinecone in a RAG-pipeline context, RAGStackGuide’s Qdrant vs Milvus vs Pinecone comparison is the companion read.

FAQ

is qdrant faster than milvus?

No independent benchmark settles it at cluster scale. Qdrant’s own 2024 benchmark reports Qdrant with the highest RPS and lowest latency in almost all scenarios, and Milvus fastest at building indexes. Zilliz, Milvus’s developer, sponsors VectorDBBench. Run filtered recall@10 and p99 at matched recall on your own queries, since filters are where the engines diverge.

is qdrant or milvus better for multi-tenant apps?

Both work, and both can keep tenants in a single collection. Qdrant filters on a tenant key indexed with is_tenant: true; Milvus uses a partition key that routes tenants into 16 partitions by default, with database, collection or partition isolation as heavier options. For strict per-tenant isolation, Weaviate’s shard-per-tenant model scales further.

can milvus run without kubernetes?

Yes. Milvus Lite installs with pip install -U pymilvus[milvus-lite] and runs inside a Python process on Ubuntu or macOS, using only FLAT indexes. Milvus Standalone runs on Docker Compose with etcd and MinIO, and the docs size it up to 100 million vectors. Only Milvus Distributed expects a Kubernetes cluster.

does qdrant support gpu search like milvus?

No. Qdrant’s GPU support, added in v1.13.0 with NVIDIA and AMD Docker images, covers index building; its docs describe no GPU search path. Milvus serves queries from GPU indexes such as GPU_CAGRA, and its docs say they pay off under high request pressure or large query batches, not as a single-query latency cut.

how much does qdrant cloud cost compared to zilliz cloud?

Both start free with room for about one million 768-dimension vectors (checked October 2026). Qdrant Cloud’s paid Standard tier bills hourly for vCPU, memory and storage, with no list price on its pricing page. Zilliz Cloud gives work-email signups $100 in credits for 30 days, plus another $100 and a one-year expiry once a payment method is added.

Sources

  1. Qdrant docs: indexing (filterable HNSW, payload indexes, ACORN)
  2. Qdrant docs: quantization (scalar, binary, product, TurboQuant)
  3. Qdrant docs: distributed deployment (Raft, shards, replication_factor)
  4. Qdrant docs: multitenancy with payload partitioning
  5. Qdrant vector search benchmarks (vendor)
  6. Weaviate docs: vector index reference (HNSW defaults, flatSearchCutoff, filterStrategy)
  7. Weaviate docs: vector quantization (PQ, BQ, SQ, RQ)
  8. Weaviate docs: data structure and multi-tenancy
  9. Weaviate docs: replication architecture
  10. Milvus docs: deployment options (Lite, Standalone, Distributed)
  11. Milvus docs: in-memory index types
  12. Milvus docs: GPU index types
  13. Milvus docs: multi-tenancy strategies
  14. Milvus release notes (3.0.x)
  15. VectorDBBench, benchmark sponsored by Zilliz (vendor)
  16. ANN-Benchmarks (Aumüller, Bernhardsson, Faithfull), independent
  17. Malkov and Yashunin, HNSW (arXiv 1603.09320)
  18. Patel et al., ACORN: predicate-agnostic search over vector embeddings and structured data (arXiv 2403.04871)
  19. Ootomo et al., CAGRA: parallel graph construction and ANN search for GPUs (arXiv 2308.15136)
  20. Qdrant releases on GitHub (v1.19.x)
  21. Qdrant docs: quickstart (Docker ports 6333 and 6334)
  22. Qdrant docs: payload
  23. Qdrant docs: points (ID types)
  24. Qdrant docs: vectors (datatypes, sparse, multivectors)
  25. Qdrant docs: running with GPU
  26. Qdrant Cloud docs: create a cluster (free tier limits)
  27. Qdrant pricing
  28. Qdrant migration tool (GitHub)
  29. Milvus docs: run Milvus Standalone with Docker Compose
  30. Milvus docs: requirements for running Milvus with Docker
  31. Milvus docs: Milvus Lite
  32. Milvus docs: limits
  33. Milvus docs: schema and field types
  34. Milvus docs: dynamic field
  35. Milvus docs: mmap storage
  36. Milvus docs: consistency levels
  37. Zilliz Cloud docs: try Zilliz Cloud for free
  38. Zilliz VTS (Vector Transport Service) on GitHub
  39. Weaviate docs: model provider integrations
  40. Milvus docs: HNSW index (default M and efConstruction)
  41. Milvus release notes (2.6.x)
#qdrant#weaviate#milvus #vector-database #hnsw #comparison

Related