Back to Blog

ChromaDB vs Qdrant vs Weaviate vs Milvus: Field Notes from Building a Client for All Four

July 22, 2026 4 min read
ChromaDB vs Qdrant vs Weaviate vs Milvus: Field Notes from Building a Client for All Four

Not a benchmark. The connection models, metric vocabularies, batch limits, and data shapes we hit writing one adapter per database.


Most "vector DB comparison" posts are benchmarks - numbers-per-second on someone else's hardware with someone else's data, which rarely predicts your workload. This isn't that.

 

We wrote client adapters for ChromaDB, Qdrant, Weaviate, and Milvus - one app that speaks to all four - so we met each database as an integrator: its connection model, its metric vocabulary, its batch ceiling, and the shape it hands records back in. Those are the things that actually shape your code. Here are the notes, straight off the adapters.

The metric vocabulary is different in every one

This is the first thing that bites when you write for all four. "Cosine" is universal; almost nothing else is. Here is the exact set each adapter accepts:

  • ChromaDB - cosine, l2, ip
  • Qdrant - cosine, euclid, dot, manhattan
  • Milvus - cosine, l2, ip
  • Weaviate - cosine, dot, l2-squared, hamming, manhattan

Look at what that does to a supposedly simple idea. "Euclidean" is l2 in Chroma and Milvus, euclid in Qdrant, and l2-squared in Weaviate - three spellings and, in Weaviate's case, a squared variant that isn't even the same function. "Dot product" is ip (inner product) in Chroma/Milvus and dot in Qdrant/Weaviate. Weaviate alone offers hamming; Qdrant and Weaviate offer manhattan and the others don't. A metric string you copy from one database's tutorial is not portable, and a naive "map cosine→cosine, l2→l2" layer will silently mishandle three of these four.

The record shape is different in every one, too

Writing a generic Document type over four databases means four different unwraps. Concretely:

  • ChromaDB returns parallel arrays - { ids[], documents[], metadatas[], embeddings[] } - that you zip back into records by index. The document text is a first-class field.
  • Qdrant returns { id, payload, vector }. There is no dedicated text field - the document lives inside payload (we look for payload.text or payload.document), and id can come back as a number or a string.
  • Weaviate returns { id, class, properties, vector } - a class (its word for a collection) plus properties holding your metadata; id is a UUID.
  • Milvus returns { id, fields, vector }, with metadata under fields.

The practical upshot: "get me the text of this document" is four different code paths. In Chroma it's a column; in the other three it's a convention inside a metadata bag, and if your ingestion didn't put the text under a key the reader expects, the GUI shows an embedding with no readable document. That single inconsistency generated more adapter code than anything else.

Batch limits and capabilities you'll actually hit

From each adapter's declared capabilities:

ChromaDB Qdrant Weaviate Milvus
Default port 8000 6333 8080 19530
Max batch size 41,666 1,000 1,000 1,000
Hybrid search yes no yes no
Max dimensions 50,000 65,536 65,535 32,768
Metric spellings l2/ip euclid/dot l2-squared/dot l2/ip

That Chroma batch number isn't a typo - its practical insert ceiling is far higher than the others', which changes how you chunk bulk loads. And hybrid (vector + keyword) search being present in Chroma and Weaviate but absent in Qdrant and Milvus is the kind of capability gap that quietly dictates architecture if you assumed it was universal.

So which do you reach for?

Field notes, not a leaderboard:

  • ChromaDB - lowest ceremony, embedded or lightweight server, huge batch ceiling. Prototyping and local dev. Watch its metric default; it's the one people forget to set.
  • Qdrant - explicit, predictable API; first-class payload filtering; the most pleasant of the four to write against. Production retrieval without a heavyweight cluster. (No hybrid search out of the box.)
  • Weaviate - schema-first "platform" posture: classes, modules, GraphQL. Power if you want the DB to own more of the pipeline; more concepts if you just want a vector store.
  • Milvus - the heavyweight, most infra to stand up, widest index selection, built for very large scale. When you actually have that scale and the team to run it.

The one thing they share

Every one of these sets the distance metric at collection-creation time and won't let you change it in place - and, as the table shows, they don't even agree on what to call the metrics. That combination is a portability trap: a workflow copied between databases can silently inherit a metric you never chose. The two-second habit that prevents it is reading the collection's declared metric before you trust a single result.

I hit every one of these quirks building VectorLens, a native desktop GUI that connects to all four with one interface - which is also why it normalizes those four record shapes into one readable view. It runs locally and talks directly to whichever database you point it at: vectorlens.dev.

The honest summary: there's no "best" vector DB - there's the one whose connection model, metric vocabulary, and operational weight fit your team. Knowing all four well enough to choose deliberately beats any benchmark that doesn't match your data.

Share: