The Vector DB Trap
You’ve built a RAG chatbot. You threw embeddings into ChromaDB, it works great on your laptop, and you ship it. Then production hits. You need filtering by date range, backups that don’t require copying Python dictionaries, or query performance that doesn’t crater when you scale past 10,000 documents.
That’s when you realize: there’s more than one way to store vectors. A lot more.
ChromaDB is fine for prototypes. But Qdrant brings Rust-fast search with serious filtering. Weaviate adds schema validation and hybrid search. The three aren’t interchangeable — they’re answering different questions about what a vector database needs to be.
Let’s build something with each one, see where they shine, and figure out which one you actually need.
What Vector Databases Actually Do (The Brief Version)
A vector database stores embeddings (dense numerical vectors from an LLM) and answers one question fast: “Give me the N documents most similar to this query vector.”
That’s the core job. Everything else — filtering, persistence, replication, schema enforcement — is about how well they do it in production.
ChromaDB: The Dead Simple Choice
The pitch: Zero setup, lives in your Python process, perfect for notebooks and prototypes.
Reality: It works, until it doesn’t. No persistence out of the box (it writes to disk but forgets everything on restart if you’re not careful). Filtering is basic. Performance drops fast when you hit scale. But for a proof-of-concept? Chef’s kiss.
ChromaDB Docker Setup
version: "3.8"services: chroma: image: ghcr.io/chroma-core/chroma:latest ports: - "8000:8000" volumes: - chroma_data:/chroma/data environment: IS_PERSISTENT: "TRUE"
volumes: chroma_data:Spin it up:
docker compose up -dChromaDB Python Example
import chromadb
# Connect to serverclient = chromadb.HttpClient(host="localhost", port=8000)
# Create collectioncollection = client.get_or_create_collection(name="articles")
# Add documents with embeddingscollection.add( ids=["doc1", "doc2"], documents=["Docker networking is complex", "Kubernetes is overkill for home labs"], metadatas=[{"topic": "docker"}, {"topic": "k8s"}],)
# Query (Chroma generates embeddings automatically with default model)results = collection.query( query_texts=["Tell me about container networking"], n_results=1,)
print(results["documents"])Simple. Boring. Works.
Qdrant: The Production Speed Demon
The pitch: Written in Rust, built for speed. Filtering works. Snapshots for backup. REST and gRPC APIs.
Reality: It’s a real database. More to learn, but you’re not wondering if it’ll survive a restart or scale beyond 100K vectors.
Qdrant is what you graduate to when ChromaDB starts feeling like a toy. It’s fast because it’s compiled, well-designed, and doesn’t compromise on filtering.
Qdrant Docker Setup
version: "3.8"services: qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" - "6334:6334" volumes: - qdrant_storage:/qdrant/storage environment: QDRANT_API_KEY: "your-secret-key"
volumes: qdrant_storage:Qdrant Python Example
from qdrant_client import QdrantClientfrom qdrant_client.models import Distance, VectorParams, PointStructimport numpy as np
client = QdrantClient(url="http://localhost:6333", api_key="your-secret-key")
# Create collection with vector dimension (768 for common embeddings)client.create_collection( collection_name="articles", vectors_config=VectorParams(size=768, distance=Distance.COSINE),)
# Add points with filtering metadataclient.upsert( collection_name="articles", points=[ PointStruct( id=1, vector=np.random.rand(768).tolist(), payload={"title": "Docker Compose", "topic": "docker", "year": 2026}, ), PointStruct( id=2, vector=np.random.rand(768).tolist(), payload={"title": "Kubernetes Operators", "topic": "k8s", "year": 2025}, ), ],)
# Search with filtering (year >= 2025)results = client.search( collection_name="articles", query_vector=np.random.rand(768).tolist(), query_filter={ "must": [ { "key": "year", "range": {"gte": 2025}, } ] }, limit=5,)
for hit in results: print(f"{hit.payload['title']} (score: {hit.score})")See that query_filter? That’s why people choose Qdrant. Filtering isn’t a hack — it’s baked in.
Why Qdrant Wins on Performance
- Rust: Compiled, not interpreted. Memory efficient.
- HNSW index: Hierarchical Navigable Small World — search is O(log n), not linear.
- Filtering before distance calc: Most of the slow work is skipped for vectors that don’t match your filters.
- Snapshots: Built-in backup. No “copy the database directory” nonsense.
Trade-off: You need to manage another service. But if you’re self-hosting anyway, that’s just one more container.
Weaviate: The ORM for Vectors
The pitch: Schema-first, GraphQL queries, built-in vectorizers (no separate embedding step), can do hybrid search (keyword + semantic together).
Reality: Heavier on resources, more opinionated about structure. Powerful if you need it, overkill if you don’t.
Weaviate lets you define your data structure upfront. It can generate embeddings for you on ingest. It can search using both traditional keywords and vectors in one query. It’s almost like an ORM met a vector database and had a very overqualified baby.
Weaviate Docker Setup
version: "3.8"services: weaviate: image: semitechnologies/weaviate:latest ports: - "8080:8080" - "50051:50051" volumes: - weaviate_data:/var/lib/weaviate environment: QUERY_DEFAULTS_LIMIT: 100 AUTHENTICATION_APIKEY_ENABLED: "true" AUTHENTICATION_APIKEY_ALLOWED_KEYS: "weaviate-key" DEFAULT_VECTORIZER_MODULE: "text2vec-openai" OPENAI_APIKEY: ${OPENAI_API_KEY}
volumes: weaviate_data:You’ll need an OpenAI key, but Weaviate also supports Hugging Face, Cohere, and local vectorizers.
Weaviate Python Example
import weaviatefrom weaviate.classes.config import Configure
client = weaviate.connect_to_local( host="localhost", port=8080, grpc_port=50051,)
# Define schema (like creating a table)client.collections.create( name="Article", description="Blog articles with metadata", vectorizer_config=Configure.Vectorizer.text2vec_openai(), properties=[ Configure.Property.text(name="title", description="Article title"), Configure.Property.text(name="body", description="Article content"), Configure.Property.text(name="topic", description="Category"), Configure.Property.int(name="year", description="Publication year"), ],)
# Add objects (schema enforced)articles = client.collections.get("Article")articles.data.insert_multiple( [ { "title": "Docker Compose Secrets", "body": "How to handle sensitive data in Compose files...", "topic": "docker", "year": 2026, }, { "title": "Kubernetes Node Affinity", "body": "Controlling pod placement with node selectors...", "topic": "k8s", "year": 2025, }, ])
# GraphQL query (hybrid search: keyword + semantic)response = articles.query.hybrid( query="Docker networking and secrets", where={ "path": ["year"], "operator": "GreaterThanEqual", "valueInt": 2025, }, limit=5,)
for obj in response.objects: print(f"{obj.properties['title']} (score: {obj.metadata.score})")
client.close()Weaviate’s GraphQL layer is both its strength and its barrier. Powerful, but you need to learn GraphQL syntax if you haven’t already.
The Filtering Difference Matters
ChromaDB: Supports where-clauses, but they’re basic. Good enough for “is this tag present” or “is this number in range” — but performance degrades with complex filters on large datasets.
Qdrant: Filtering is native. Complex nested filters, range queries, all work efficiently because the index is aware of metadata from the start.
Weaviate: Filtering works through GraphQL, which is clean once you learn it. Performance is solid because Weaviate is designed for it.
Persistence and Backup
| Feature | ChromaDB | Qdrant | Weaviate |
|---|---|---|---|
| Persistence | Disk (HTTP mode) | Snapshots built-in | Persistent volumes |
| Backup story | Copy the directory | snapshot_create API | Docker volume snapshots |
| Point-in-time recovery | No | Via snapshots | No |
| Replication | No | Cluster mode (paid) | No |
Decision Time
Use ChromaDB if:
- You’re prototyping in a notebook
- You have <10K vectors
- You don’t need filtering
- You’re okay managing state outside the database
Use Qdrant if:
- You need production reliability
- You want filtering to not tank performance
- You’re self-hosting (one container, no BigTech nonsense)
- You want snapshots for backup
- Scale is 10K–10M+ vectors
Use Weaviate if:
- You need hybrid search (keywords + vectors together)
- You want the database to vectorize documents for you
- You have a complex schema that benefits from validation
- You’re willing to trade some performance for structure
The Real Talk
Most RAG projects that fail do so because they outgrew ChromaDB around month three and didn’t have a plan to migrate. Qdrant doesn’t have that problem. You can start with Chroma, port to Qdrant in a weekend, and sleep better knowing snapshots exist.
Weaviate is the choice when you’re building something that’s half-vector, half-traditional search. If you’re 90% vectors and 10% keyword search, Qdrant + Elasticsearch is cleaner than fighting Weaviate’s schema opinions.
Pick the simplest tool that doesn’t make you want to scream in production. That’s usually Qdrant.