Vector Databases Deep Dive: Qdrant, Pinecone, Weaviate & Milvus Compared

Everything you need to choose, deploy, and optimize a vector database for production RAG and AI applications.

Introduction

Vector databases are the backbone of modern AI applications — RAG, semantic search, recommendations, anomaly detection. But they're not all the same.

Why Vector Databases?

Traditional DB Vector DB
Exact match (B-tree) Approximate Nearest Neighbor (ANN)
Scalar values High-dimensional vectors (768-4096 dims)
SQL queries Distance metrics (cosine, dot, L2)
Millions of rows Billions of vectors
ACID transactions Eventual consistency (usually)

Core Concepts

Vector Similarity Search

# similarity_basics.py import numpy as np def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def dot_product(a: np.ndarray, b: np.ndarray) -> float: return np.dot(a, b) def l2_distance(a: np.ndarray, b: np.ndarray) -> float: return np.linalg.norm(a - b) # Normalized vectors: cosine == dot product # For cosine, always normalize embeddings first! 

ANN Algorithms

Algorithm Principle Build Time Search Speed Accuracy Memory
HNSW Hierarchical graph Medium Very Fast High High
IVF Inverted file (clustering) Fast Fast Medium Medium
PQ Product quantization Medium Very Fast Medium Low
DiskANN Disk-based graph Slow Fast High Very Low
Flat Brute force None Slow Perfect High

HNSW is the default for most production use cases.

Database Comparison

Quick Comparison

Feature Qdrant Pinecone Weaviate Milvus Chroma
License Apache 2.0 Proprietary BSD-3 Apache 2.0 Apache 2.0
Self-hosted ✅ ❌ ✅ ✅ ✅
Managed Cloud ✅ ✅ ✅ ✅ (Zilliz) ✅
Filtering Excellent Good Excellent Excellent Good
Hybrid Search ✅ ✅ ✅ ✅ ⚠️
Multi-tenancy ✅ ✅ ✅ ✅ ❌
Replication ✅ Managed ✅ ✅ ❌
GPU Support ✅ ❌ ❌ ✅ ❌
Best For Production, self-host Serverless, ease of use Graph + vector, GraphQL Massive scale, GPU Prototyping, local dev

Detailed: Qdrant (Recommended for Self-Hosted)

# qdrant_complete.py from qdrant_client import QdrantClient from qdrant_client.models import ( Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue, MatchAny, SearchParams, HnswConfigDiff, QuantizationConfig, ScalarQuantization, ScalarQuantizationConfig, OptimizersConfigDiff, CollectionStatus ) from sentence_transformers import SentenceTransformer import uuid class QdrantVectorDB: def __init__(self, url: str = "http://localhost:6333", api_key: str = None): self.client = QdrantClient(url=url, api_key=api_key) self.encoder = SentenceTransformer("BAAI/bge-large-en-v1.5") def create_collection(self, name: str, vector_size: int = 1024): """Create optimized collection for production.""" self.client.create_collection( collection_name=name, vectors_config=VectorParams( size=vector_size, distance=Distance.COSINE, hnsw_config=HnswConfigDiff( m=16, # Connections per layer (16-64)  ef_construct=100 # Build accuracy (100-200)  ), quantization_config=QuantizationConfig( scalar=ScalarQuantization( type=ScalarQuantizationConfig( quantile=0.99, always_ram=True ) ) ), on_disk=False # Keep hot vectors in RAM  ), optimizers_config=OptimizersConfigDiff( default_segment_number=2, max_segment_size=20000, memmap_threshold=20000, indexing_threshold=10000, flush_interval_sec=5 ) ) def upsert_documents(self, collection: str, documents: list[dict]): """Batch upsert with embeddings.""" texts = [d["text"] for d in documents] vectors = self.encoder.encode(texts, show_progress_bar=True) points = [ PointStruct( id=str(uuid.uuid4()), vector=vec.tolist(), payload={ "text": doc["text"], "metadata": doc.get("metadata", {}), "timestamp": doc.get("timestamp", "") } ) for vec, doc in zip(vectors, documents) ] self.client.upsert(collection_name=collection, points=points) def search( self, collection: str, query: str, limit: int = 10, filters: dict = None, score_threshold: float = 0.7 ) -> list[dict]: query_vector = self.encoder.encode([query])[0] query_filter = None if filters: conditions = [ FieldCondition(key=k, match=MatchValue(value=v)) for k, v in filters.items() ] query_filter = Filter(must=conditions) results = self.client.search( collection_name=collection, query_vector=query_vector.tolist(), limit=limit, query_filter=query_filter, score_threshold=score_threshold, search_params=SearchParams( hnsw_ef=128, # Search accuracy (higher = more accurate)  exact=False ) ) return [ { "id": r.id, "score": r.score, "text": r.payload["text"], "metadata": r.payload["metadata"] } for r in results ] def hybrid_search( self, collection: str, query: str, query_vector: list[float] = None, limit: int = 10, sparse_weight: float = 0.3 ) -> list[dict]: """Hybrid dense + sparse search (requires sparse vectors).""" # Requires collection with sparse vectors enabled  # This is simplified - full hybrid needs sparse vector config  if query_vector is None: query_vector = self.encoder.encode([query])[0].tolist() # Use prefetch for hybrid  from qdrant_client.models import Prefetch, FusionQuery results = self.client.query_points( collection_name=collection, prefetch=[ Prefetch( query=query_vector, using="dense", limit=limit * 2 ) ], query=FusionQuery(fusion="rrf"), # Reciprocal Rank Fusion  limit=limit ) return [ {"id": r.id, "score": r.score, "text": r.payload["text"]} for r in results.points ] def delete_by_filter(self, collection: str, filters: dict): """Delete points matching filter.""" conditions = [ FieldCondition(key=k, match=MatchValue(value=v)) for k, v in filters.items() ] self.client.delete( collection_name=collection, points_selector=Filter(must=conditions) ) def scroll_all(self, collection: str, limit: int = 100): """Iterate all points.""" offset = None while True: points, offset = self.client.scroll( collection_name=collection, limit=limit, offset=offset, with_payload=True, with_vectors=False ) for p in points: yield p if offset is None: break 

Detailed: Pinecone (Serverless, Managed)

# pinecone_complete.py from pinecone import Pinecone, ServerlessSpec from sentence_transformers import SentenceTransformer import uuid class PineconeVectorDB: def __init__(self, api_key: str, environment: str = "us-east-1"): self.pc = Pinecone(api_key=api_key) self.encoder = SentenceTransformer("BAAI/bge-large-en-v1.5") self.environment = environment def create_index(self, name: str, dimension: int = 1024, metric: str = "cosine"): if name not in self.pc.list_indexes().names(): self.pc.create_index( name=name, dimension=dimension, metric=metric, spec=ServerlessSpec( cloud="aws", region=self.environment ) ) self.index = self.pc.Index(name) def upsert_documents(self, documents: list[dict], namespace: str = ""): texts = [d["text"] for d in documents] vectors = self.encoder.encode(texts) vectors_to_upsert = [ { "id": str(uuid.uuid4()), "values": vec.tolist(), "metadata": { "text": doc["text"], **doc.get("metadata", {}) } } for vec, doc in zip(vectors, documents) ] # Batch upsert (100 at a time)  for i in range(0, len(vectors_to_upsert), 100): batch = vectors_to_upsert[i:i+100] self.index.upsert(vectors=batch, namespace=namespace) def search( self, query: str, limit: int = 10, namespace: str = "", filters: dict = None ) -> list[dict]: query_vector = self.encoder.encode([query])[0].tolist() filter_dict = None if filters: filter_dict = {k: {"$eq": v} for k, v in filters.items()} results = self.index.query( vector=query_vector, top_k=limit, namespace=namespace, filter=filter_dict, include_metadata=True ) return [ { "id": r.id, "score": r.score, "text": r.metadata.pop("text", ""), "metadata": r.metadata } for r in results.matches ] 

Detailed: Weaviate (GraphQL, Hybrid Search)

# weaviate_complete.py import weaviate from weaviate.classes.init import Auth from weaviate.classes.config import Configure, Property, DataType from sentence_transformers import SentenceTransformer class WeaviateVectorDB: def __init__(self, url: str, api_key: str = None): auth = Auth.api_key(api_key) if api_key else None self.client = weaviate.connect_to_wcs( cluster_url=url, auth_credentials=auth ) self.encoder = SentenceTransformer("BAAI/bge-large-en-v1.5") def create_collection(self, name: str): self.client.collections.create( name=name, vectorizer_config=Configure.Vectorizer.none(), # Bring your own vectors  properties=[ Property(name="text", data_type=DataType.TEXT), Property(name="source", data_type=DataType.TEXT), Property(name="category", data_type=DataType.TEXT), ], vector_index_config=Configure.VectorIndex.hnsw( distance_metric=Configure.VectorIndex.DistanceMetric.COSINE ) ) def upsert_documents(self, collection: str, documents: list[dict]): coll = self.client.collections.get(collection) with coll.batch.dynamic() as batch: for doc in documents: vector = self.encoder.encode(doc["text"]) batch.add_object( properties={ "text": doc["text"], "source": doc.get("metadata", {}).get("source", ""), "category": doc.get("metadata", {}).get("category", "") }, vector=vector.tolist() ) def hybrid_search( self, collection: str, query: str, limit: int = 10, alpha: float = 0.5, # 0 = keyword, 1 = vector  filters: dict = None ) -> list<...
Source:https://dev.to/ipsphil_digital/vector-databases-deep-dive-qdrant-pinecone-weaviate-milvus-compared-1694