Vector databases (Pinecone, Qdrant, Weaviate)
On this page
Objectives
- Understand why vector databases are necessary
- Compare Pinecone, Qdrant, Weaviate, Chroma, and pgvector
- Implement search with each of them
Why a vector database?
The NumPy index from the previous lesson works perfectly for a few thousand documents because an exhaustive scan of ten thousand vectors remains imperceptible. At one million, that same calculation becomes a second of latency per query, and the matrix no longer fits comfortably in memory.
Four needs emerge together. ANN search (Approximate Nearest Neighbors) abandons the guarantee of an exact result in favor of a millisecond response on millions of vectors—a compromise that is almost always acceptable, since returning the fifth-best document instead of the fourth changes nothing for the user experience. Metadata filtering combined with vector search lets you restrict a query to a category, language, or date range without post-processing. Persistence and real-time updates avoid rebuilding an entire file just to add a document. Finally, horizontal scaling is what distinguishes a prototype from a production service.
Pinecone
Pinecone is a managed service: you provision no servers, you create an index by specifying its dimension and metric, and the rest is handled for you. Notice the categorie filter passed directly to query—vector search and filtering execute together, not one after the other.
from pinecone import Pinecone
from openai import OpenAI
pc = Pinecone(api_key="your-pinecone-key")
openai_client = OpenAI()
# Create an index
pc.create_index(
name="my-corpus",
dimension=3072,
metric="cosine",
spec={"serverless": {"cloud": "aws", "region": "eu-west-1"}}
)
index = pc.Index("my-corpus")
# Index documents
documents = [
{"id": "doc1", "texte": "Python Installation Guide",
"categorie": "tutorial"},
{"id": "doc2", "texte": "Deploying a Flask Application",
"categorie": "tutorial"},
{"id": "doc3", "texte": "Q1 2026 Quarterly Report",
"categorie": "report"},
]
# Generate embeddings
textes = [d["texte"] for d in documents]
response = openai_client.embeddings.create(
input=textes,
model="text-embedding-3-large"
)
# Upsert into Pinecone
vecteurs = [
{
"id": doc["id"],
"values": emb.embedding,
"metadata": {"texte": doc["texte"], "categorie": doc["categorie"]}
}
for doc, emb in zip(documents, response.data)
]
index.upsert(vectors=vecteurs)
# Search
query_emb = openai_client.embeddings.create(
input="how to install Python",
model="text-embedding-3-large"
).data[0].embedding
resultats = index.query(
vector=query_emb,
top_k=3,
include_metadata=True,
filter={"categorie": {"$eq": "tutorial"}}
)
for match in resultats.matches:
print(f"[{match.score:.3f}] {match.metadata['texte']}")
The dimension declared at creation is final: switching from text-embedding-3-large to text-embedding-3-small requires recreating the index. So decide on the model before, not after.
Qdrant
Qdrant is open source, self-hosted, and offers excellent performance. The vocabulary differs—we talk about collections, points, and payloads where Pinecone speaks of indexes, vectors, and metadata—but the schema remains identical. Note that the example calls the embedding API document by document inside the loop: this is acceptable for three documents, but think about switching back to batches as soon as the corpus grows.
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct, Filter,
FieldCondition, MatchValue
)
from openai import OpenAI
qclient = QdrantClient(url="http://localhost:6333")
openai_client = OpenAI()
# Create a collection
qclient.create_collection(
collection_name="my-corpus",
vectors_config=VectorParams(
size=3072,
distance=Distance.COSINE
)
)
# Index documents
points = []
for i, doc in enumerate(documents):
emb = openai_client.embeddings.create(
input=doc["texte"],
model="text-embedding-3-large"
).data[0].embedding
points.append(PointStruct(
id=i,
vector=emb,
payload={"texte": doc["texte"], "categorie": doc["categorie"]}
))
qclient.upsert(collection_name="my-corpus", points=points)
# Search with filter
query_emb = openai_client.embeddings.create(
input="deploy a web application",
model="text-embedding-3-large"
).data[0].embedding
resultats = qclient.search(
collection_name="my-corpus",
query_vector=query_emb,
limit=3,
query_filter=Filter(
must=[FieldCondition(
key="categorie",
match=MatchValue(value="tutorial")
)]
)
)
for r in resultats:
print(f"[{r.score:.3f}] {r.payload['texte']}")
Weaviate
Weaviate stands out for its native hybrid search, which we will revisit in lesson 9. Its data model is more structured than that of its competitors: you explicitly declare properties and their types before inserting anything, which brings the experience closer to that of a classic database.
import weaviate
from openai import OpenAI
wclient = weaviate.connect_to_local()
openai_client = OpenAI()
# Create a collection
from weaviate.classes.config import Property, DataType
collection = wclient.collections.create(
name="Document",
properties=[
Property(name="texte", data_type=DataType.TEXT),
Property(name="categorie", data_type=DataType.TEXT),
]
)
# Index
for doc in documents:
emb = openai_client.embeddings.create(
input=doc["texte"],
model="text-embedding-3-large"
).data[0].embedding
collection.data.insert(
properties={"texte": doc["texte"], "categorie": doc["categorie"]},
vector=emb
)
# Search
query_emb = openai_client.embeddings.create(
input="install Python",
model="text-embedding-3-large"
).data[0].embedding
resultats = collection.query.near_vector(
near_vector=query_emb,
limit=3
)
Chroma and pgvector
Chroma aims at prototyping: a persistent client pointed at a local folder, a collection created on the fly, no infrastructure. This is the option that takes you from a NumPy index to a real database without changing your working environment.
import chromadb
from openai import OpenAI
chroma = chromadb.PersistentClient(path="./chroma_db")
openai_client = OpenAI()
collection = chroma.get_or_create_collection("my-corpus")
# Index
for doc in documents:
emb = openai_client.embeddings.create(
input=doc["texte"],
model="text-embedding-3-large"
).data[0].embedding
collection.add(
ids=[doc["id"]],
embeddings=[emb],
documents=[doc["texte"]],
metadatas=[{"categorie": doc["categorie"]}]
)
# Search
query_emb = openai_client.embeddings.create(
input="how to install Python",
model="text-embedding-3-large"
).data[0].embedding
resultats = collection.query(query_embeddings=[query_emb], n_results=3)
pgvector addresses a very different situation: your documents are already in PostgreSQL, with their joins, access rights, and backups. Rather than introducing a second system to synchronize, the extension adds a vector type and distance operators to the existing database. The <=> operator calculates cosine distance, and the HNSW index is what makes the query usable beyond a few thousand rows.
-- pgvector extension
CREATE EXTENSION vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
texte TEXT,
categorie TEXT,
embedding vector(3072)
);
-- HNSW index for performance
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
-- Search for the 5 nearest neighbors
SELECT id, texte, 1 - (embedding <=> $1::vector) AS score
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 5;
Comparison
| Criterion | Pinecone | Qdrant | Weaviate | Chroma | pgvector |
|---|---|---|---|---|---|
| Type | Managed | Open source | Open source | Open source | Extension |
| Hosting | Cloud | Self-hosted/Cloud | Self-hosted/Cloud | Local/Embed | PostgreSQL |
| Hybrid search | ✅ | ✅ | ✅ Native | ❌ | Via SQL |
| Production | ✅ | ✅ | ✅ | ⚠️ Prototyping | ✅ |
| Ease of use | ✅✅ | ✅ | ✅ | ✅✅ | ✅ |
The choice depends less on raw performance, which is very close in practice, than on your context: a team that already runs PostgreSQL has every interest in staying with it, a team with no ops will choose managed, and a team that must host in Europe for regulatory reasons will turn to self-hosted.
Summary
- Chroma for rapid prototyping
- pgvector if you already have PostgreSQL
- Pinecone for managed, no infrastructure
- Qdrant for high-performance self-hosting
- Weaviate for native hybrid search