# pgvector vs Qdrant vs Chroma: Postgres or a dedicated vector database?

Start with pgvector if you already run Postgres - the vectors sit in the same tables, transactions and backups as the rest of your data, and a filter is an ordinary WHERE clause. Choose Qdrant when vector search is a large or central workload that needs filtering inside the search, built-in quantization, sharding and replication. Choose Chroma for the quickest start in Python, in memory or in a folder, with a managed cloud when it grows. All 3 use HNSW indexes, so at small sizes they return the same neighbours; the differences show in operations, scale and filtering.

- Page: https://tecyfy.com/ai-agents/pgvector-vs-qdrant-vs-chroma
- Part of: How AI agents work (https://tecyfy.com/ai-agents)

- **pgvector** (Open source, a Postgres extension, https://github.com/pgvector/pgvector): An extension that adds vector types, distance operators and HNSW and IVFFlat indexes to Postgres, so vectors are columns in ordinary tables. Install: `CREATE EXTENSION vector;`
- **Qdrant** (Qdrant, open source under Apache 2.0, https://qdrant.tech/documentation/overview/): A vector search engine written in Rust that stores points, each a vector with a JSON payload, and searches them with filters, run as a server or in Qdrant Cloud. Install: `pip install qdrant-client`
- **Chroma** (Chroma, open source under Apache 2.0, https://docs.trychroma.com/docs/overview/introduction): An open-source search database for AI apps that runs inside a Python program, as a server, or as Chroma Cloud, a managed distributed service. Install: `pip install chromadb`

A [Vector Database](https://tecyfy.com/concepts/vector-database) stores [embeddings](https://tecyfy.com/concepts/embedding), lists of numbers that stand for the meaning of a piece of text, and finds the ones closest to a question. That is the retrieval step of [RAG](https://tecyfy.com/concepts/rag): embed the question, find the nearest chunks, put them in the prompt. Any of these 3 can do it. The real question is whether vector search should live in the database you already run, or in a system of its own.

## pgvector: vectors as a column in Postgres

[pgvector](https://tecyfy.com/concepts#pgvector) adds a `vector` type to Postgres, distance operators such as `<=>` for cosine distance, and 2 kinds of index: [HNSW](https://tecyfy.com/concepts/hnsw) and IVFFlat. An embedding becomes a column in the table that holds the chunk it came from, so the chunk, its document, its owner and its embedding are written in one transaction and backed up together.

Filtering is SQL. The catch with any approximate index is that it searches a fixed number of candidates and then the filter removes some, so a strict filter can return fewer rows than you asked for. Since version 0.8.0, iterative scans fix the common case: the index keeps searching until enough rows pass the filter. For a filter with only a few values, such as a language, a partial index per value also works.

Its limits are those of 1 Postgres server. The index wants to sit in memory, it indexes vectors of up to 2,000 numbers at full precision, and growing past 1 machine means replicas for reads or sharding with something like Citus.

## Qdrant: a server built for filtered search

Qdrant stores points: a vector plus a JSON payload. The payload holds what you filter on, such as a customer id, a language or a permission group, and filters run inside the graph search rather than after it, using payload indexes you create on those fields. That is why Qdrant holds up when filters are strict and many, such as search limited to what one user may see.

It also does the things a large index needs on its own: scalar quantization stores each number in 1 byte instead of 4, binary quantization in a bit, and collections can be split into shards and replicated across nodes. Sparse vectors for keywords sit beside the dense ones, so 1 query can match both meaning and exact words. For development, the Python client runs Qdrant in memory or in a folder with no server.

## Chroma: the quickest start

Chroma runs inside your Python program. `chromadb.Client()` gives an in-memory database and `PersistentClient` keeps it in a folder, with nothing else to install or start. It can also embed text for you with a default model, which is why so many tutorials and notebooks begin with it. Filters are `where` clauses on metadata, and `where_document` matches the text itself.

The open-source version is a single node. For larger collections, Chroma Cloud runs a distributed version behind the same API, so code written against the local database moves over unchanged.

## Side by side

| | pgvector | Qdrant | Chroma |
| --- | --- | --- | --- |
| **Where the vectors live** | In a column of a Postgres table, next to the rows they describe. | In collections of points on a Qdrant server, each point a vector plus a JSON payload. | In collections inside the Python process, in a folder on disk, on a Chroma server or in Chroma Cloud. |
| **Filtering** | A WHERE clause. With an approximate index, iterative scans keep searching until enough rows pass the filter. | Filters on payload fields run inside the search, sped up by payload indexes on the fields you filter on. | A where clause on metadata, with operators such as $eq and $in, and where_document to match the text itself. |
| **Indexes** | HNSW and IVFFlat. Up to 2,000 dimensions indexed at full precision, 4,000 at half precision. | HNSW on vectors, plus payload indexes for filters and sparse vector indexes for keyword search. | HNSW on 1 machine and SPANN in Distributed Chroma and Chroma Cloud, with the distance (l2 by default, cosine or ip) set per collection. |
| **Shrinking vectors** | Half-precision and bit types, used in an expression index, with re-ranking by the full vectors. | Built-in scalar (4 times smaller), binary (up to 32 times) and product quantization. | No quantization setting among its collection options. |
| **Keyword and hybrid search** | Postgres full-text search in the same query, combined with Reciprocal Rank Fusion in SQL. | Dense and sparse vectors in one query, fused with RRF or DBSF. | Text matching with $contains and $regex; sparse vectors for keyword search in Chroma Cloud. |
| **Transactions and joins** | Yes. Postgres transactions, joins, point-in-time recovery and every Postgres tool. | No joins. The payload carries what the search needs; other data stays in your database. | No joins. Metadata carries what the search needs. |
| **Growing past 1 machine** | Read replicas, or sharding with Citus or another approach. Most teams scale up 1 server first. | Built-in sharding and replication across nodes, with resharding and rebalancing in Qdrant Cloud. | Chroma Cloud runs a distributed version; the open-source server is 1 node. |
| **Run it locally** | Any Postgres with the extension, from a laptop to a managed service. | In memory or in a folder from the Python client, or the qdrant/qdrant Docker image. | chromadb.Client() in memory, or PersistentClient with a folder, with nothing else to start. |

## The same search on each

The same search on each: store 5 help articles with their embeddings and language, then find the 2 nearest to a question, among English articles only. The embeddings are made-up 4-number vectors so each file runs without a model. All 3 were run first and print the same 2 articles with the same cosine distances.

### pgvector: `search_pgvector.py`

```python
import os

import numpy as np
import psycopg
from pgvector.psycopg import register_vector

# Help articles with made-up 4-number embeddings; a real model gives hundreds or thousands of numbers.
ARTICLES = [
    (1, "Reset your password", "en", [0.9, 0.1, 0.0, 0.1]),
    (2, "Passwort zurücksetzen", "de", [0.88, 0.12, 0.0, 0.1]),
    (3, "Change your plan", "en", [0.1, 0.9, 0.1, 0.0]),
    (4, "Download your invoices", "en", [0.0, 0.2, 0.9, 0.1]),
    (5, "Turn on 2-step sign-in", "en", [0.7, 0.0, 0.1, 0.6]),
]
QUESTION = np.array([0.85, 0.1, 0.05, 0.2])  # "I forgot my password", embedded by the same model

conn = psycopg.connect(os.environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432/postgres"), autocommit=True)
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
register_vector(conn)

conn.execute("CREATE TABLE articles (id bigint PRIMARY KEY, title text, lang text, embedding vector(4))")
conn.execute("CREATE INDEX ON articles USING hnsw (embedding vector_cosine_ops)")
with conn.cursor() as cur:
    cur.executemany(
        "INSERT INTO articles VALUES (%s, %s, %s, %s)",
        [(id, title, lang, np.array(vector)) for id, title, lang, vector in ARTICLES],
    )

# A filter is an ordinary WHERE. Iterative scans keep the index searching until enough rows pass it.
conn.execute("SET hnsw.iterative_scan = relaxed_order")
rows = conn.execute(
    "SELECT id, title, embedding <=> %s AS distance FROM articles WHERE lang = 'en' ORDER BY distance LIMIT 2",
    (QUESTION,),
).fetchall()
for id, title, distance in rows:
    print(id, title, round(distance, 3))
```

The vectors are a column, the index is CREATE INDEX and the filter is WHERE. The <=> operator is cosine distance. Set DATABASE_URL to your Postgres; pip install "psycopg[binary]" pgvector numpy.

### Qdrant: `search_qdrant.py`

```python
from qdrant_client import QdrantClient, models

# Help articles with made-up 4-number embeddings; a real model gives hundreds or thousands of numbers.
ARTICLES = [
    (1, "Reset your password", "en", [0.9, 0.1, 0.0, 0.1]),
    (2, "Passwort zurücksetzen", "de", [0.88, 0.12, 0.0, 0.1]),
    (3, "Change your plan", "en", [0.1, 0.9, 0.1, 0.0]),
    (4, "Download your invoices", "en", [0.0, 0.2, 0.9, 0.1]),
    (5, "Turn on 2-step sign-in", "en", [0.7, 0.0, 0.1, 0.6]),
]
QUESTION = [0.85, 0.1, 0.05, 0.2]  # "I forgot my password", embedded by the same model

# In memory for this example; QdrantClient(url="http://localhost:6333") for a server.
client = QdrantClient(":memory:")
client.create_collection(
    "articles", vectors_config=models.VectorParams(size=4, distance=models.Distance.COSINE)
)
client.upsert(
    "articles",
    points=[
        models.PointStruct(id=id, vector=vector, payload={"title": title, "lang": lang})
        for id, title, lang, vector in ARTICLES
    ],
)

# The filter runs inside the search, on the payload stored with each point.
hits = client.query_points(
    "articles",
    query=QUESTION,
    query_filter=models.Filter(must=[models.FieldCondition(key="lang", match=models.MatchValue(value="en"))]),
    limit=2,
).points
for hit in hits:
    print(hit.id, hit.payload["title"], round(1 - hit.score, 3))
```

Each point carries its own payload, and the filter is part of the search request. Qdrant returns cosine similarity, so the file prints 1 minus the score to match the other 2.

### Chroma: `search_chroma.py`

```python
import chromadb

# Help articles with made-up 4-number embeddings; a real model gives hundreds or thousands of numbers.
ARTICLES = [
    (1, "Reset your password", "en", [0.9, 0.1, 0.0, 0.1]),
    (2, "Passwort zurücksetzen", "de", [0.88, 0.12, 0.0, 0.1]),
    (3, "Change your plan", "en", [0.1, 0.9, 0.1, 0.0]),
    (4, "Download your invoices", "en", [0.0, 0.2, 0.9, 0.1]),
    (5, "Turn on 2-step sign-in", "en", [0.7, 0.0, 0.1, 0.6]),
]
QUESTION = [0.85, 0.1, 0.05, 0.2]  # "I forgot my password", embedded by the same model

# In memory for this example; chromadb.PersistentClient(path="db") keeps it on disk.
client = chromadb.Client()
collection = client.create_collection("articles", configuration={"hnsw": {"space": "cosine"}})
collection.add(
    ids=[str(id) for id, _, _, _ in ARTICLES],
    embeddings=[vector for _, _, _, vector in ARTICLES],
    metadatas=[{"title": title, "lang": lang} for _, title, lang, _ in ARTICLES],
)

# The filter is a where clause on the metadata stored with each item.
found = collection.query(query_embeddings=[QUESTION], n_results=2, where={"lang": "en"})
for id, meta, distance in zip(found["ids"][0], found["metadatas"][0], found["distances"][0]):
    print(id, meta["title"], round(distance, 3))
```

Chroma's default distance is squared L2, so the collection asks for cosine. Without the embeddings argument, Chroma would embed the text itself with its default model.

The 3 files return the same 2 articles at the same distances, because each builds an HNSW index on [cosine distance](https://tecyfy.com/concepts/cosine-similarity) over the same vectors. The differences are in where things live: pgvector filters a column of a table, Qdrant a payload on each point, Chroma metadata on each item. Note the German article: it is the second-closest vector of all, and only the filter keeps it out.

## Which to choose

- **We already run Postgres:** pgvector. No new system to run, back up or secure. Vectors join the rows they belong to, and a document and its chunks are written in one transaction.
- **Strict filters, by customer or by permission:** pgvector or Qdrant. pgvector filters with WHERE, partial indexes and iterative scans; Qdrant filters inside the search with payload indexes. Both keep the filter in the query, not after it.
- **Hundreds of millions of vectors:** Qdrant. Built-in quantization keeps the index in less memory, and sharding and replication spread it over several nodes.
- **A prototype in a notebook today:** Chroma or Qdrant. Both run inside Python with nothing to start, in memory or in a folder. Chroma has the shortest path from install to query.
- **Keyword and meaning in one search:** pgvector or Qdrant. pgvector pairs with Postgres full-text search, and Qdrant fuses dense and sparse vectors in one query.
- **No database to run at all:** Qdrant or Chroma. Qdrant Cloud and Chroma Cloud run the database for you, behind the same API as the open-source versions.

## Things to watch

- **Use the same model for documents and questions.** Vectors from different embedding models, or different versions of one, cannot be compared. Changing the model means embedding everything again.
- **Filters and approximate search.** An approximate index finds candidates first. With a strict filter, check that you get as many results as you asked for, and turn on iterative scans in pgvector or payload indexes in Qdrant.
- **Measure recall on your own data.** Defaults trade accuracy for speed. Compare the index's answers with an exact search on a sample of real questions before you tune.
- **Plan memory before you load.** The index should fit in memory for fast search. Work out the size first with the [vector database size calculator](/ai-agents/vector-database-size), and see what embedding the data costs with the [embedding cost calculator](/prices/embedding-cost).

To see where retrieval sits in a whole agent, follow one request through [How AI agents work](/ai-agents).

## Questions

### Is pgvector good enough for production?

For most RAG apps, yes. It brings HNSW indexes, filtering with iterative scans, half-precision and bit vectors, and everything Postgres already gives - transactions, joins, backups and point-in-time recovery. The limits to plan for are the 2,000 dimensions it indexes at full precision and the memory the index needs on 1 server; past that, replicas, sharding or a dedicated database come in.

### When should I use a dedicated vector database instead of pgvector?

When vector search is a large or central workload on its own - hundreds of millions of vectors, heavy filtered search traffic, or a need for built-in quantization, sharding and replication across nodes. A dedicated database such as Qdrant is built for that; pgvector shares a server with the rest of your Postgres work.

### What is the difference between Qdrant and Chroma?

Qdrant is a server written in Rust built for filtered search at scale, with payload indexes, quantization, sharding and replication, and a local mode in its Python client for development. Chroma aims at the quickest start in Python, running in-process with nothing to set up, and its managed Chroma Cloud runs a distributed version for larger collections.

### What about Pinecone?

Pinecone is a managed vector database only - there is nothing to install or run yourself. It runs as a serverless service on AWS, Google Cloud and Azure, with data organised into indexes and namespaces. It fits teams that want no database to operate and accept a hosted service.

### Do all vector databases give the same results?

With the same embeddings, the same distance and an exact search, yes. With an approximate index such as HNSW, each returns its best guess, and results can differ slightly, especially with filters. Tune the search settings, such as ef_search in pgvector, and measure recall on your own data rather than trusting defaults.

### How much memory does a vector database need?

Mostly the size of its index - the vectors' numbers times their bytes, plus the HNSW links. 55,000 vectors of 1,536 numbers take about 345 MB at full precision, and half precision or binary quantization shrinks that. The vector database size calculator works it out from what you have.

## Sources

- [pgvector: open-source vector similarity search for Postgres](https://github.com/pgvector/pgvector)
- [Qdrant overview](https://qdrant.tech/documentation/overview/)
- [Qdrant on GitHub](https://github.com/qdrant/qdrant)
- [Filtering](https://qdrant.tech/documentation/concepts/filtering/)
- [Quantization](https://qdrant.tech/documentation/guides/quantization/)
- [Hybrid and multi-stage queries](https://qdrant.tech/documentation/concepts/hybrid-queries/)
- [Distributed deployment](https://qdrant.tech/documentation/scaling/distributed_deployment/)
- [Qdrant Python client](https://github.com/qdrant/qdrant-client)
- [Local quickstart](https://qdrant.tech/documentation/quickstart/)
- [Introduction](https://docs.trychroma.com/docs/overview/introduction)
- [Chroma clients](https://docs.trychroma.com/docs/run-chroma/clients)
- [Configure collections](https://docs.trychroma.com/docs/collections/configure)
- [Metadata filtering](https://docs.trychroma.com/docs/querying-collections/metadata-filtering)
- [Full text search and regex](https://docs.trychroma.com/docs/querying-collections/full-text-search)
- [Embedding functions](https://docs.trychroma.com/docs/embeddings/embedding-functions)
- [Sparse vector search setup](https://docs.trychroma.com/cloud/schema/sparse-vector-search)
- [Chroma Cloud](https://docs.trychroma.com/cloud/getting-started)
- [Pinecone database architecture](https://docs.pinecone.io/guides/get-started/database-architecture)
