DEV Community

Peyton Green
Peyton Green

Posted on

Vector Databases for Python Developers: pgvector, Chroma, and Qdrant Compared (2026)

If you're building a RAG pipeline or a semantic search feature in Python, you've hit the same decision point: which vector database do I use?

In 2026, the answer depends on one question: are you optimizing for local dev speed or production scale?

Here's the honest comparison — no benchmarks from vendor blogs.


The Three Choices That Actually Matter

pgvector Chroma Qdrant
Best for Existing Postgres stack Local dev/prototyping Production self-hosted
Deployment PostgreSQL extension Embedded or server Docker or cloud
Python client psycopg + pgvector chromadb qdrant-client
Filtering SQL (full power) Metadata filters Rich payload filters
Persistence Postgres (ACID) File-based or in-memory WAL-based
Free tier Yes (self-hosted) Yes (open source) Yes (open source)
Managed cloud Supabase, Neon None (self-hosted only) Qdrant Cloud

pgvector — If You Already Run Postgres

pgvector turns your existing PostgreSQL database into a vector store. No new infrastructure, no separate service, no operational overhead.

pip install psycopg pgvector
Enter fullscreen mode Exit fullscreen mode
import psycopg
from pgvector.psycopg import register_vector
import numpy as np

conn = psycopg.connect("postgresql://user:pass@localhost/mydb")

# The extension must exist before register_vector() can find the vector type
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
conn.commit()
register_vector(conn)

# Create table with vector column
conn.execute("""
    CREATE TABLE IF NOT EXISTS documents (
        id SERIAL PRIMARY KEY,
        content TEXT,
        embedding vector(1536)
    )
""")

# Insert document with embedding
embedding = np.array(get_embedding("my document text"))  # 1536-dim for text-embedding-3-small
conn.execute(
    "INSERT INTO documents (content, embedding) VALUES (%s, %s)",
    ("my document text", embedding)
)
conn.commit()

# Similarity search (cosine distance)
results = conn.execute("""
    SELECT content, 1 - (embedding <=> %s::vector) AS similarity
    FROM documents
    ORDER BY embedding <=> %s::vector
    LIMIT 5
""", (embedding, embedding)).fetchall()
Enter fullscreen mode Exit fullscreen mode

When pgvector wins:

  • You already have Postgres in production
  • You need SQL joins between vectors and relational data (e.g., WHERE user_id = ?)
  • You want ACID guarantees on your vector data
  • You don't want to run a separate service

When pgvector loses:

  • You need ANN (approximate nearest neighbor) at scale — pgvector's HNSW is good but not Qdrant-level
  • Your embeddings dimension exceeds ~2000 (pgvector performance degrades above this)
  • You need advanced payload filtering with complex conditions

Chroma — For Local Dev and Prototyping

Chroma is the fastest path from "I have documents" to "I have semantic search." It runs in-process, needs no configuration, and persists to disk with one line.

pip install chromadb
Enter fullscreen mode Exit fullscreen mode
import chromadb
from chromadb.utils import embedding_functions

# Persistent client — survives process restart
client = chromadb.PersistentClient(path="./chroma_db")

# Use OpenAI or any embedding function
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key="sk-...",
    model_name="text-embedding-3-small"
)

collection = client.get_or_create_collection(
    name="documents",
    embedding_function=openai_ef
)

# Add documents — Chroma handles embedding automatically
collection.add(
    documents=["Python is a high-level language", "pgvector extends PostgreSQL"],
    metadatas=[{"source": "tutorial"}, {"source": "blog"}],
    ids=["doc1", "doc2"]
)

# Query
results = collection.query(
    query_texts=["vector storage in postgres"],
    n_results=2,
    where={"source": "blog"}  # metadata filter
)
print(results["documents"])
Enter fullscreen mode Exit fullscreen mode

When Chroma wins:

  • You're prototyping and want zero setup
  • Your dataset fits comfortably in memory/local disk (under a few million vectors)
  • You're doing research or building a demo
  • You want embedded mode (no server process)

When Chroma loses:

  • Production workloads — Chroma is not designed for multi-process or high-concurrency access
  • You need fine-grained access control
  • Dataset size exceeds ~10M vectors

Qdrant — For Production Self-Hosted

Qdrant is built for production. It ships as a single Docker container, handles billions of vectors, and has the richest filtering API of the three.

pip install qdrant-client
Enter fullscreen mode Exit fullscreen mode
from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct,
    Filter, FieldCondition, MatchValue, DatetimeRange,
)
import numpy as np

client = QdrantClient("localhost", port=6333)

# Create collection
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)

# Upsert points
embedding = get_embedding("my document text")
client.upsert(
    collection_name="documents",
    points=[
        PointStruct(
            id=1,
            vector=embedding,
            payload={
                "content": "my document text",
                "source": "blog",
                "category": "python",
                "published_at": "2026-10-01T00:00:00Z"
            }
        )
    ]
)

# Search with payload filtering
results = client.query_points(
    collection_name="documents",
    query=get_embedding("vector database tutorial"),
    query_filter=Filter(
        must=[
            FieldCondition(key="category", match=MatchValue(value="python")),
            FieldCondition(
                key="published_at",
                range=DatetimeRange(gte="2026-01-01T00:00:00Z"),
            ),
        ]
    ),
    limit=5,
    with_payload=True,
)
for point in results.points:
    print(point.score, point.payload["content"])
Enter fullscreen mode Exit fullscreen mode

When Qdrant wins:

  • You need production-grade reliability and HNSW performance
  • Complex filtering logic (nested conditions, ranges, geo)
  • You want on-premise / self-hosted with no usage-based pricing
  • Billion-scale vector collections

When Qdrant loses:

  • You already run Postgres and don't want another service (use pgvector)
  • You're in prototype phase and want zero setup (use Chroma)
  • You need managed cloud with zero ops (use Qdrant Cloud or Pinecone)

The Decision Tree

Are you in production?
├─ No → Is your dataset > 1M vectors?
│       ├─ No → Chroma (embedded, fastest to start)
│       └─ Yes → Qdrant (even for dev at this scale)
└─ Yes → Do you already run Postgres?
         ├─ Yes → pgvector (no new service)
         └─ No → Qdrant (best ANN + filtering at scale)
Enter fullscreen mode Exit fullscreen mode

Embedding the Embeddings — Don't Compute Twice

Regardless of which vector database you use, the most expensive operation is generating embeddings. Cache them.

import hashlib
import json
from functools import lru_cache

# Simple disk cache for embeddings
def get_cached_embedding(text: str, cache_dir: str = ".embedding_cache") -> list[float]:
    cache_key = hashlib.md5(text.encode()).hexdigest()
    cache_file = f"{cache_dir}/{cache_key}.json"

    try:
        with open(cache_file) as f:
            return json.load(f)
    except FileNotFoundError:
        pass

    # Only calls the API on cache miss
    embedding = client.embeddings.create(
        input=text,
        model="text-embedding-3-small"
    ).data[0].embedding

    os.makedirs(cache_dir, exist_ok=True)
    with open(cache_file, "w") as f:
        json.dump(embedding, f)

    return embedding
Enter fullscreen mode Exit fullscreen mode

For production, replace the file cache with Redis or a proper KV store. The pattern is the same.


Evaluating Your Vector Store Choice

Once you have vectors in a database, the next question is: how do you know your similarity search is actually returning good results?

This is where most developers skip a critical step. "It looks right" isn't a metric.

The LLM Evaluation Workflow Pack includes a ready-to-run RAGAS evaluation harness that works with all three databases covered here. It measures retrieval precision, recall, and faithfulness — the three metrics that actually predict whether your RAG pipeline will fail in production.

Plug it in before you ship.


What's Next

If you're building a RAG pipeline and need to go beyond the vector store setup, the next article in this series covers the full production walkthrough: chunking strategies, embedding model selection, retrieval patterns, and the common failure modes. → RAG in Python Without the Bloat: A Production Walkthrough (coming Nov 3)


peytongreen_dev — I build Python tools for developer workflows. Find my production-tested templates at kazdispatch.gumroad.com.

Top comments (0)