Vector Databases: Pinecone vs Weaviate vs Chroma
Compare Pinecone, Weaviate, and Chroma vector databases for RAG applications. Covers setup, performance, pricing, and when to use each one.
What you'll learn
- ✓How vector databases work and why RAG needs them
- ✓Setting up Pinecone, Weaviate, and Chroma with Python
- ✓Performance and scaling characteristics of each database
- ✓How to choose the right vector database for your use case
Prerequisites
- •Understanding of embeddings and similarity search
- •Python fundamentals
- •Basic familiarity with RAG concepts
Vector databases are the backbone of every RAG system. They store embeddings, perform similarity search at scale, and return the documents your LLM needs for grounded responses. But choosing between them can be confusing. Pinecone is fully managed, Weaviate offers hybrid search, and Chroma is lightweight and local. This guide gives you hands-on experience with all three so you can make an informed decision.
How Vector Databases Work
A vector database stores high-dimensional vectors (embeddings) and supports fast approximate nearest neighbor (ANN) search. When you query with a vector, it finds the most similar vectors in the collection without comparing against every single entry.
The key algorithms behind this speed are:
- HNSW (Hierarchical Navigable Small World): Builds a multi-layer graph for fast traversal. Used by Weaviate, Chroma, and pgvector.
- IVF (Inverted File Index): Partitions vectors into clusters and searches only the nearest clusters. Used by FAISS.
- Tree-based methods: Used by Pinecone’s proprietary engine for their managed service.
All three databases we compare support metadata filtering, batch operations, and Python SDKs.
Chroma: The Local-First Option
Chroma is designed for developers who want to get started quickly. It runs in-process with no server needed, stores data to disk, and requires zero configuration.
import chromadb
from chromadb.utils import embedding_functions
# Create a persistent client (data saved to disk)
client = chromadb.PersistentClient(path="./chroma_data")
# Use OpenAI embeddings (or Chroma's default all-MiniLM-L6-v2)
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
model_name="text-embedding-3-small"
)
# Create a collection
collection = client.get_or_create_collection(
name="documents",
embedding_function=openai_ef,
metadata={"hnsw:space": "cosine"}
)
# Add documents (embeddings generated automatically)
documents = [
"Python is a versatile programming language used in web development and data science.",
"JavaScript powers interactive web pages and runs in every modern browser.",
"Rust provides memory safety without garbage collection through its ownership system.",
"Go was designed at Google for building scalable network services.",
"TypeScript adds static typing to JavaScript for better developer experience.",
]
collection.add(
documents=documents,
ids=[f"doc_{i}" for i in range(len(documents))],
metadatas=[
{"language": "python", "category": "general-purpose"},
{"language": "javascript", "category": "web"},
{"language": "rust", "category": "systems"},
{"language": "go", "category": "systems"},
{"language": "typescript", "category": "web"},
]
)
# Query with automatic embedding
results = collection.query(
query_texts=["systems programming with memory safety"],
n_results=3
)
for doc, score, metadata in zip(
results["documents"][0],
results["distances"][0],
results["metadatas"][0]
):
print(f" [{score:.4f}] {doc}")
print(f" Metadata: {metadata}")
# Query with metadata filtering
web_results = collection.query(
query_texts=["best language for web development"],
n_results=3,
where={"category": "web"}
)
print(f"\nWeb-only results: {len(web_results['documents'][0])}")
Chroma strengths: Zero setup, runs in-process, great for prototyping and small datasets, automatic embedding generation, good Python API.
Chroma limitations: Single-node only, no built-in replication, performance degrades past a few million vectors, limited production tooling.
Pinecone: The Fully Managed Option
Pinecone is a cloud-native vector database. You do not run any infrastructure; everything is managed via their API. This makes it the easiest path to production for teams that do not want to manage database infrastructure.
from pinecone import Pinecone, ServerlessSpec
from openai import OpenAI
import time
# Initialize Pinecone
pc = Pinecone(api_key="your-pinecone-api-key")
openai_client = OpenAI()
# Create an index
index_name = "documents"
if index_name not in pc.list_indexes().names():
pc.create_index(
name=index_name,
dimension=1536, # Match your embedding model
metric="cosine",
spec=ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
# Wait for index to be ready
while not pc.describe_index(index_name).status["ready"]:
time.sleep(1)
index = pc.Index(index_name)
# Generate embeddings
def embed_texts(texts: list[str]) -> list[list[float]]:
response = openai_client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return [item.embedding for item in response.data]
# Upsert documents with metadata
documents = [
{"id": "doc_0", "text": "Python is a versatile programming language.", "language": "python"},
{"id": "doc_1", "text": "JavaScript powers interactive web pages.", "language": "javascript"},
{"id": "doc_2", "text": "Rust provides memory safety without GC.", "language": "rust"},
{"id": "doc_3", "text": "Go was designed for scalable services.", "language": "go"},
{"id": "doc_4", "text": "TypeScript adds static typing to JS.", "language": "typescript"},
]
texts = [d["text"] for d in documents]
embeddings = embed_texts(texts)
vectors = [
{
"id": doc["id"],
"values": emb,
"metadata": {"text": doc["text"], "language": doc["language"]}
}
for doc, emb in zip(documents, embeddings)
]
index.upsert(vectors=vectors)
print(f"Upserted {len(vectors)} vectors")
# Query
query_embedding = embed_texts(["systems programming with memory safety"])[0]
results = index.query(
vector=query_embedding,
top_k=3,
include_metadata=True
)
for match in results.matches:
print(f" [{match.score:.4f}] {match.metadata['text']}")
# Query with metadata filter
filtered_results = index.query(
vector=query_embedding,
top_k=3,
include_metadata=True,
filter={"language": {"$in": ["rust", "go"]}}
)
Pinecone strengths: Zero infrastructure management, scales to billions of vectors, built-in replication and high availability, serverless pricing option, fast query latency.
Pinecone limitations: Cloud-only (no self-hosting), vendor lock-in, cost grows with scale, limited query capabilities beyond vector search.
Weaviate: The Hybrid Search Option
Weaviate combines vector search with traditional keyword (BM25) search and offers a GraphQL API. It can run locally via Docker or as a managed cloud service.
import weaviate
from weaviate.classes.init import Auth
from weaviate.classes.config import Configure, Property, DataType
from weaviate.classes.query import MetadataQuery
# Connect to local Weaviate (run: docker run -p 8080:8080 semitechnologies/weaviate)
client = weaviate.connect_to_local()
# Or connect to Weaviate Cloud
# client = weaviate.connect_to_weaviate_cloud(
# cluster_url="https://your-cluster.weaviate.network",
# auth_credentials=Auth.api_key("your-api-key"),
# headers={"X-OpenAI-Api-Key": "your-openai-key"}
# )
# Create a collection with vectorizer
collection = client.collections.create(
name="Document",
vectorizer_config=Configure.Vectorizer.text2vec_openai(
model="text-embedding-3-small"
),
properties=[
Property(name="text", data_type=DataType.TEXT),
Property(name="language", data_type=DataType.TEXT),
Property(name="category", data_type=DataType.TEXT),
]
)
# Add documents (Weaviate generates embeddings automatically)
documents = [
{"text": "Python is a versatile programming language.", "language": "python", "category": "general-purpose"},
{"text": "JavaScript powers interactive web pages.", "language": "javascript", "category": "web"},
{"text": "Rust provides memory safety without GC.", "language": "rust", "category": "systems"},
{"text": "Go was designed for scalable services.", "language": "go", "category": "systems"},
{"text": "TypeScript adds static typing to JS.", "language": "typescript", "category": "web"},
]
with collection.batch.dynamic() as batch:
for doc in documents:
batch.add_object(properties=doc)
# Vector search (semantic)
results = collection.query.near_text(
query="systems programming with memory safety",
limit=3,
return_metadata=MetadataQuery(distance=True)
)
for obj in results.objects:
print(f" [{obj.metadata.distance:.4f}] {obj.properties['text']}")
# Keyword search (BM25)
bm25_results = collection.query.bm25(
query="memory safety",
limit=3
)
# Hybrid search (combines vector + keyword)
hybrid_results = collection.query.hybrid(
query="systems programming memory safety",
limit=3,
alpha=0.5 # 0 = pure keyword, 1 = pure vector
)
for obj in hybrid_results.objects:
print(f" {obj.properties['text']}")
client.close()
Weaviate strengths: Hybrid search (vector + BM25), built-in vectorization modules, GraphQL API, self-hostable with Docker, multi-tenancy support, active open-source community.
Weaviate limitations: More complex setup than Chroma, resource-heavy for local development, learning curve for GraphQL queries, some features require specific modules.
Head-to-Head Comparison
| Feature | Chroma | Pinecone | Weaviate |
|---|---|---|---|
| Hosting | Local / embedded | Cloud only | Self-hosted or cloud |
| Setup complexity | Minimal | API key only | Docker or cloud |
| Hybrid search | No | No | Yes (BM25 + vector) |
| Max scale | ~1M vectors | Billions | Millions+ |
| Pricing | Free (open source) | Free tier, then per-usage | Free (open source) or cloud |
| Replication | No | Built-in | Built-in |
| Metadata filtering | Basic | Advanced | Advanced |
| Auto-vectorization | Yes | No | Yes (with modules) |
| Best for | Prototyping, small apps | Production at scale | Hybrid search, self-hosted production |
Performance Benchmarks
Real-world performance depends on your data and queries. Here is a simple benchmarking framework to test with your own data:
import time
import numpy as np
def benchmark_database(db_client, documents: list[str], queries: list[str]):
"""Benchmark insert and query performance."""
# Benchmark insertion
start = time.perf_counter()
# Insert documents (implementation depends on the database)
insert_time = time.perf_counter() - start
# Benchmark queries
query_times = []
for query in queries:
start = time.perf_counter()
# Run query (implementation depends on the database)
elapsed = time.perf_counter() - start
query_times.append(elapsed)
return {
"insert_time_total": insert_time,
"insert_time_per_doc": insert_time / len(documents),
"query_time_avg": np.mean(query_times),
"query_time_p50": np.percentile(query_times, 50),
"query_time_p95": np.percentile(query_times, 95),
"query_time_p99": np.percentile(query_times, 99),
}
# Generate test data
num_docs = 10000
test_documents = [f"This is test document number {i} about topic {i % 50}" for i in range(num_docs)]
test_queries = [f"topic {i}" for i in range(100)]
In typical benchmarks with 100K documents and 1536-dimensional vectors:
- Chroma: ~5ms p50 query latency, simple to set up, memory usage grows linearly
- Pinecone: ~10-20ms p50 query latency (includes network), consistent at any scale
- Weaviate: ~5-10ms p50 query latency (local), hybrid search adds ~2-5ms overhead
Decision Framework
Choose Chroma when:
- You are building a prototype or proof of concept
- Your dataset is under 500K documents
- You want the simplest possible setup
- You are building a local application or CLI tool
Choose Pinecone when:
- You need production reliability without managing infrastructure
- Your dataset is large (millions to billions of vectors)
- You want automatic scaling and high availability
- Your team does not have database operations expertise
Choose Weaviate when:
- You need hybrid search (combining keyword and semantic)
- You want to self-host for data privacy or compliance
- You need multi-tenancy for SaaS applications
- You want an open-source solution with production capabilities
Migration Between Databases
If you start with Chroma and need to migrate to Pinecone or Weaviate later, abstract your vector database behind an interface:
from abc import ABC, abstractmethod
class VectorStore(ABC):
"""Abstract interface for vector database operations."""
@abstractmethod
def add_documents(self, documents: list[str], ids: list[str], metadatas: list[dict] = None):
pass
@abstractmethod
def search(self, query: str, top_k: int = 5, filters: dict = None) -> list[dict]:
pass
@abstractmethod
def delete(self, ids: list[str]):
pass
class ChromaStore(VectorStore):
def __init__(self, collection_name: str):
self.client = chromadb.PersistentClient(path="./data")
self.collection = self.client.get_or_create_collection(collection_name)
def add_documents(self, documents, ids, metadatas=None):
self.collection.add(documents=documents, ids=ids, metadatas=metadatas)
def search(self, query, top_k=5, filters=None):
kwargs = {"query_texts": [query], "n_results": top_k}
if filters:
kwargs["where"] = filters
results = self.collection.query(**kwargs)
return [
{"id": id, "text": doc, "score": dist, "metadata": meta}
for id, doc, dist, meta in zip(
results["ids"][0], results["documents"][0],
results["distances"][0], results["metadatas"][0]
)
]
def delete(self, ids):
self.collection.delete(ids=ids)
# Swap implementations without changing application code
# store = ChromaStore("my_collection")
# store = PineconeStore("my_index")
# store = WeaviateStore("MyCollection")
Wrapping Up
The vector database landscape is maturing rapidly, but the decision does not need to be overwhelming. Start with Chroma for development and prototyping because it has zero friction. Move to Pinecone when you need managed production infrastructure. Choose Weaviate when hybrid search or self-hosting is a requirement. Whichever you choose, abstract the database behind an interface so migration remains straightforward. The most important factor is not which database you pick but how well your embeddings and chunking strategy work, because a great vector database cannot compensate for poor embeddings.
Related articles
- RAG RAG in Production: Build a Reliable Pipeline
Build a production-ready RAG pipeline with document ingestion, chunking, retrieval, and generation. Covers error handling, monitoring, and optimization.
- RAG RAG Chunk Overlap Strategies
Learn how chunk overlap rescues boundary context in RAG pipelines, with practical strategies for choosing overlap size and shape for different corpora.
- RAG RAG Chunking Strategies Explained
Compare fixed-size, sentence, semantic, and structural chunking for retrieval augmented generation and pick the right one for your corpus.
- RAG RAG Hybrid Search: BM25 + Vectors
Combine lexical BM25 with dense vector search to recover the queries each method misses on its own and boost RAG retrieval quality.