Retrieval Is a Spectrum, Not a Product Category
Every RAG pipeline, semantic-search feature, and recommendation engine eventually runs into the same wall: you have 10 million embeddings and you need the 20 nearest ones in under 30 milliseconds. The confusing part is that the four tools people reach for — FAISS, pgvector, Pinecone, and Qdrant — are not four versions of the same thing. They sit at different points on a spectrum that runs from “a library you embed in your process” to “a database you run, operate, and pay for.”
Picking wrong is expensive. Picking FAISS when you need a multi-tenant, filterable, horizontally-scalable store means you are about to hand-roll a database. Picking Pinecone when your data is 50,000 vectors and already lives in Postgres means you are paying monthly fees to cache what an index on your existing table could do.
This post compares them on the axes that actually decide production outcomes: what they are, how you operate them, latency and recall, filtering, the operational story, and the real total cost. Every section has runnable code so you can reproduce the claims instead of taking my word for it.
What Each One Actually Is
Before comparing numbers, get the ontology straight, because it explains almost every difference below.
- FAISS is a C++ library with Python bindings. It is not a database. It runs inside your process, holds indexes in RAM (or on disk), and has no notion of servers, users, or persistent storage. It is the search kernel that other tools are built on or measured against.
- pgvector is an extension to PostgreSQL. It does not run anywhere on its own; it adds a
vectorcolumn type and an index to a Postgres you already run. Your vector search lives next to your relational data, in the same transaction and the same query. - Pinecone is a fully managed, proprietary SaaS. You call an HTTP API. Someone else runs the cluster, does the scaling, and takes your credit card. You never touch the infrastructure.
- Qdrant is an open-source vector database (with a managed cloud). It is a dedicated server process you can self-host or rent, with its own storage engine, filtering, and REST/gRPC API.
# A mental model that maps tool -> responsibility:
# FAISS : in-process index, no server, no persistence built-in
# pgvector : a column type + index inside Postgres
# Pinecone : managed REST API, zero infra you control
# Qdrant : its own database server, self-hosted or cloud
The defining question for each tool is who is responsible when something goes wrong at scale: the FAISS answer is “you, in your code”; the pgvector answer is “you, via Postgres”; the Pinecone answer is “the vendor”; the Qdrant answer is “you, unless you buy the cloud.”
Grounding: The Dataset and Embeddings We’ll Use
To keep the comparison honest, everything below uses the same dataset: 500,000 vectors of 384 dimensions (a typical output size for all-MiniLM-L6-v2 or BAAI/bge-small-en). I generate them with numpy so the numbers are about the search engines, not about a particular embedding model.
# make_vectors.py — build a deterministic, realistic test set
import numpy as np
rng = np.random.default_rng(42)
DIM = 384
N = 500_000
# Random unit-ish vectors plus a few genuine "clusters" so ANN has signal.
vectors = rng.standard_normal((N, DIM)).astype("float32")
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
# Attach metadata so we can exercise filtered search later.
metadata = np.arange(N) # a numeric "tenant_id" / "partition" style field
A 384-dim float32 vector is 1,536 bytes. Half a million of them is roughly 766 MB before any index overhead — large enough to feel the difference between in-memory FAISS and a disk-backed server, small enough to run on a laptop.
FAISS: The Search Kernel
FAISS is the reference implementation for approximate nearest neighbor (ANN) search, and it is where the algorithmic vocabulary comes from. You configure two things: an index type (the data structure) and a search parameter (how much approximation you allow).
The Four Indexes You’ll Actually Use
IndexFlatL2— exhaustive, exact, brute force. Every query scores every vector. Perfect recall, O(N) per query. Fine for a few hundred thousand vectors in RAM.IndexIVFFlat— inverted-file index. Partitions vectors intonlistclusters via k-means, then only searches the closest few clusters (nprobe) at query time. The standard first ANN upgrade.IndexIVFPQ— adds product quantization on top of IVF: it compresses each vector into compact codes, so the index fits in a fraction of the memory. Great for very large corpora, at a recall cost.IndexHNSWFlat— hierarchical navigable small-world graph. Builds a graph where each node links to its neighbors at several resolutions. The fastest high-recall choice today, at a memory cost (the graph is big).
# faiss_basics.py — the four indexes, one snippet each
import faiss
import numpy as np
DIM, N = 384, 500_000
vectors = np.random.standard_normal((N, DIM)).astype("float32")
# 1. Exhaustive exact search. No approximation, brute-force scoring.
flat = faiss.IndexFlatL2(DIM)
flat.add(vectors) # ~766 MB resident
# 2. IVF + Flat: cluster into 4096 cells, probe 16 at query time.
nlist = 4096
ivf = faiss.IndexIVFFlat(flat, DIM, nlist, faiss.METRIC_L2)
ivf.train(vectors) # k-means on a sample
ivf.add(vectors)
ivf.nprobe = 16 # cells searched per query
# 3. IVF + Product Quantization: compress vectors to ~8 bytes each.
pq = faiss.IndexIVFPQ(flat, DIM, nlist, 8, 8) # 8 subquantizers, 8 bits
pq.train(vectors)
pq.add(vectors)
pq.nprobe = 32
# 4. HNSW graph: typically the best speed/recall trade-off.
hnsw = faiss.IndexHNSWFlat(DIM, 32) # M = 32 neighbors
hnsw.hnsw.efConstruction = 200 # build quality
hnsw.add(vectors)
hnsw.hnsw.efSearch = 64 # query-time search breadth
# faiss_query.py — exact and approximate results on the same point
q = vectors[7:8] # query vector, shape (1, DIM)
# Exact: recall is definitionally 1.0.
D_exact, I_exact = flat.search(q, k=20)
# Approximate: D, I are distances and ids of the k nearest.
D_approx, I_approx = hnsw.search(q, k=20)
# Self-query sanity check: the top hit should be the point itself.
assert I_exact[0][0] == 7
Pros and Cons of FAISS
Pros
- Raw performance leader. Written in C++ with SIMD, it is the fastest thing in this comparison at equal index settings.
- Every index algorithm, tunable. IVF, PQ, HNSW, scalar quantizers, and combinations. You can squeeze the exact memory/recall/latency trade-off you want.
- Zero operational cost. No server, no network, no credentials — it is
pip installand go. Ideal for batch jobs and offline deduplication. - In-process. No serialization across a socket for every query. Great when your vectors already live in Python.
Cons
- Not a database. No persistence guarantees, no durability, no crash recovery. You must write the index to disk and reload it yourself.
- No built-in filtering or metadata. Filtering by a field is a manual, painful exercise: you either do post-filtering (search then drop) or pre-filtering (break vectors into per-filter-class FAISS indexes) — both have sharp trade-offs.
- Single-node, in-memory. Indexes live in RAM. Scaling past one machine means you become the distributed-systems engineer.
- You own the ops. Concurrency, sharding, replication, backups, monitoring, eviction — all yours.
# The filtering gap in FAISS: you search first, then check metadata.
# If too many neighbors fail the filter, recall silently collapses.
import numpy as np
ids = np.arange(vectors.shape[0])
def filtered_search(flat_index, q, tenant_keep, k=20):
D, I = flat_index.search(q, k * 4) # over-fetch to compensate
kept = [i for i in I[0] if tenant_keep(i)][:k]
return kept
# Pros: exact, zero extra infra. Cons: you wrote the filter, recall is on you.
pgvector: Search Where Your Data Already Lives
pgvector is the answer to the most common real question: “I already run Postgres. Do I really need a second database for 200,000 embeddings?” Usually no. pgvector adds a vector type, distance operators, and indexes straight into the Postgres you operate.
What pgvector Gives You
- A
vector(n)column type. - Distance operators
<->(L2),<=>(cosine),<#>(inner product). - An IVFFlat index (
ivfflat) and the newer HNSW index. - Since it is just Postgres, everything relational comes for free:
JOIN,WHEREfilters, transactions, RBAC, backups.
-- Enable the extension once per database.
CREATE EXTENSION IF NOT EXISTS vector;
-- A typical semantic-search table: the vector plus its metadata.
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
chunk TEXT,
tenant TEXT NOT NULL,
embedding vector(384)
);
-- HNSW index. This is the index you want for most workloads.
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
# pgvector_write.py — via psycopg
import psycopg
from pgvector.psycopg import register_vector
with psycopg.connect("dbname=rag host=localhost") as conn:
register_vector(conn)
with conn.cursor() as cur:
for i in range(100_000):
vec = [0.0] * 384
cur.execute(
"INSERT INTO documents (chunk, tenant, embedding) VALUES (%s, %s, %s)",
(f"doc {i}", f"tenant-{i % 20}", vec),
)
conn.commit()
# pgvector_search.py — hybrid filtering in one SQL statement
import psycopg
from pgvector.psycopg import register_vector
q = [0.01] * 384 # a real query embedding
with psycopg.connect("dbname=rag host=localhost") as conn:
register_vector(conn)
with conn.cursor() as cur:
# Filter by tenant at the same time as searching — one round trip.
cur.execute(
"""
SELECT chunk, embedding <=> %s::vector AS dist
FROM documents
WHERE tenant = %s
ORDER BY embedding <=> %s::vector
LIMIT 20
""",
(q, "tenant-3", q),
)
rows = cur.fetchall()
The headline feature is the WHERE tenant = 'tenant-3' in the same query as the vector search. That is native pre-filtering, and it is exactly the thing FAISS makes you build by hand. Because it is SQL, filters can combine arbitrarily — WHERE created_at > now() - interval '30 days' AND category = 'legal' — with no bespoke index layering.
How the Indexes Differ, and the HNSW Caveat
-- Option A: IVFFlat. Cheaper to build, but recall depends on list sizing.
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 1000);
-- Option B: HNSW. Slower to build, better recall/latency, but it does NOT
-- support "out of range" empty index builds the same way as IVFFlat.
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
Two practical caveats that trip people up:
- IVFFlat needs data (and a
train) to be accurate. pgvector auto-trains on a sample, but an IVFFlat index built on an empty table and populated later has poor clustering. With HNSW this matters less. - HNSW build cost and memory. Building an HNSW index in Postgres can be slow and memory-hungry on large tables, and it is a single-column index in one table — you don’t get FAISS’s fine-grained parameter tuning.
Pros and Cons of pgvector
Pros
- No new database. Your vectors live in the Postgres you already run, back up, and secure. Huge operational and mental simplification.
- Transactional consistency. Vector writes participate in the same
BEGIN/COMMITas your relational data — a guarantee none of the others provide. - Native, arbitrary filtering via SQL
WHERE, and easy joins between vectors and their source rows. - Free (open source), self-hostable, and available as an extension on most managed Postgres (AWS RDS, Supabase, Neon, Google Cloud SQL).
- Familiar tooling — psql, ORMs, existing backup/monitoring.
Cons
- Not the performance king. At very large scale and extreme latency targets, a general-purpose relational database indexing vectors is usually slower than a purpose-built store.
- Index tuning is coarse. You get IVFFlat or HNSW with a handful of knobs; no PQ, no scalar quantization, no fine-grained recall dials like FAISS.
- Scaling is Postgres scaling. Vertical first; sharding a vector index across Postgres nodes is on you.
- Embedding column can bloat. If you store fat vectors inline, table and index size grow; consider where quantization happens.
Pinecone: Managed, Opinionated, Pay Per Use
Pinecone is the “I don’t want to run anything” choice. It is a fully managed vector database behind an HTTP API. You create an index, upsert vectors, and query — the vendor owns every scaling decision underneath.
The Pinecone Shape: Namespaces, Metadata, Serverless
Pinecone’s model maps cleanly onto production needs: an index, namespaces (cheap partitioning you don’t pay per-vector for the way you do metadata filtering), and optional metadata on each vector for filtered queries.
# pinecone_demo.py
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="<your-key>")
# A serverless index: the vendor decides capacity.
pc.create_index(
name="rag-index",
dimension=384,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
index = pc.Index("rag-index")
# Upsert with metadata for filtered search later.
vectors = [
(f"id-{i}", [0.01] * 384, {"tenant": "t1", "category": "legal"})
for i in range(10_000)
]
index.upsert(vectors=vectors)
# Query, filtering server-side on metadata.
result = index.query(
vector=[0.01] * 384,
top_k=20,
filter={"tenant": {"$eq": "t1"}},
namespace="production",
)
print(result["matches"])
Pros and Cons of Pinecone
Pros
- Zero operations. No servers, no indexing strategy to babysit. The whole promise is “upload vectors and query.”
- True managed scaling. Serverless and pod-based options scale with your data; you never resize instances.
- Reliable API with good SDKs, plus namespaces and metadata filtering out of the box.
- Predictable, if not cheap. High availability, backups, and updates are the vendor’s problem.
Cons
- It is a black box. You get an API, not control. Index internals, eviction, and exact recall behavior are opaque.
- Lock-in and egress. Your vectors live on someone else’s cluster; moving millions of vectors out is slow and costly.
- Cost at scale. Per-vector and per-query pricing accrues; for big corpora the monthly bill is real money compared to self-hosting Qdrant or pgvector.
- No SQL, no transactions, no joins. It is a vector store, period. Hybrid search over relational data means running a second database anyway.
Qdrant: The Purpose-Built Open Source Database
Qdrant is the middle path: a dedicated vector database you can run yourself or rent. Unlike FAISS it is a real server with persistence and filtering; unlike Pinecone it is open source and self-hostable; unlike pgvector it is built from the ground up for vectors rather than bolted onto a relational engine.
Getting Data In and Out
# qdrant_demo.py
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
client = QdrantClient(url="http://localhost:6333")
# Collections carry the schema and distance metric.
client.recreate_collection(
collection_name="documents",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# Upsert with a payload (metadata) attached to each point.
points = [
PointStruct(
id=i,
vector=[0.01] * 384,
payload={"tenant": f"t{i % 20}", "category": "legal"},
)
for i in range(100_000)
]
client.upsert(collection_name="documents", points=points, batch_size=512)
# qdrant_search.py — filtered nearest neighbors in one call
results = client.query_points(
collection_name="documents",
query=[0.01] * 384,
limit=20,
query_filter={
"must": [
{"key": "tenant", "match": {"value": "t3"}},
]
},
with_payload=True,
)
for hit in results.points:
print(hit.id, hit.score, hit.payload)
Qdrant’s engine combines an HNSW index with a filter-aware search: it does not naively do “search then filter”; it tracks which payload conditions apply and prunes during traversal. That makes filtered queries far more predictable than FAISS’s post-filtering and often faster than pgvector’s planner integration for complex payloads. It also offers quantization and on-disk vector storage, which FAISS’s in-memory model does not.
Pros and Cons of Qdrant
Pros
- Purpose-built performance for ANN with rich payload filtering; strong recall/latency at scale.
- Open source and self-hostable — no per-vector cloud fees if you run it yourself.
- Real database semantics: persistence, replication, sharding, snapshots, and a clean REST/gRPC API.
- Quantization and on-disk storage options let you trade accuracy for memory like FAISS, but inside a server you don’t have to build.
- Managed cloud exists when you’d rather not operate it.
Cons
- You run another service. One more database to deploy, back up, and monitor if self-hosting.
- Separate from your relational data. Joining vector results back to Postgres rows means crossing service boundaries.
- Less mature/ubiquitous than Postgres in terms of tooling and operational lore; the ecosystem is younger.
- Expert tuning still required at the extremes; defaults are good but not magical.
The Head-to-Head: Filtering, Recall, Latency
Put the four side by side and the shape of each tool becomes obvious.
| Concern | FAISS | pgvector | Pinecone | Qdrant |
|---|---|---|---|---|
| What it is | In-process library | Postgres extension | Managed SaaS | Open-source server |
| You operate it? | You (code) | You (Postgres) | Vendor | You or vendor |
| Persistence | Manual | Transactional | Managed | Built-in |
| Arbitrary filters | Manual | Native SQL | Metadata filters | Payload filters |
| SQL / joins | No | Yes | No | No |
| Transactions | No | Yes | No | Per-point writes |
| Horizontal scale | DIY | Postgres sharding | Vendor | Built-in |
| License / cost | Free | Free | Per-vector fees | Free / cloud fees |
The Recall Reality Check
A trap with all ANN tools is treating “it returned neighbors” as “it returned the correct neighbors.” Recall matters. The cheapest honest test is: compare ANN top-k against the exact brute-force top-k and measure overlap.
# recall_check.py — how much of the exact result do you actually get back?
import numpy as np
import faiss
rng = np.random.default_rng(0)
vectors = rng.standard_normal((100_000, 384)).astype("float32")
exact = faiss.IndexFlatIP(vectors.shape[1]) # inner-product, exact
exact.add(vectors)
hnsw = faiss.IndexHNSWFlat(vectors.shape[1], 32)
hnsw.add(vectors)
hnsw.hnsw.efSearch = 64
def recall_at_k(query, k=10):
_, exact_ids = exact.search(query, k)
_, approx_ids = hnsw.search(query, k)
return len(set(exact_ids[0]) & set(approx_ids[0])) / k
q = vectors[100:101]
print(f"recall@10 with efSearch=64: {recall_at_k(q):.2f}")
hnsw.hnsw.efSearch = 512 # widen search -> higher recall
print(f"recall@10 with efSearch=512: {recall_at_k(q):.2f}")
Every ANN tool exposes the same knob trade — widen the search breadth (efSearch/nprobe) to raise recall at the cost of latency, or tighten it for speed. The skill is knowing your target recall and setting the knob accordingly, whichever tool you pick.
Total Cost of Ownership, Honest Version
Cost is where the marketing falls apart, so compute it on real numbers:
- FAISS. Free software, but you own everything: the machine holding ~766 MB+ in RAM, the code that persists/reloads indexes, the concurrency layer, and the filtering. For a big corpus, the engineering salary is the real cost, not the hardware.
- pgvector. Free extension on a Postgres you already pay for. For most teams this is effectively near-zero marginal cost and is why it wins for sub-~10M-vector, Postgres-centric stacks.
- Pinecone. The simplest to start, the most expensive to keep. Costs scale with stored vectors, writes, and reads. Predictable bill, but a real monthly line item.
- Qdrant. Free if you self-host (you pay ops). Managed cloud removes ops but reintroduces subscription cost, typically below Pinecone for the same footprint.
The honest decision rule is not “which is fastest” — on a tuned benchmark they are within a small constant factor of each other. It is which cost and responsibility model fits your data volume, your filtering needs, and the team you already have.
Decision Guide
- You need the last drop of speed and control, data fits in RAM, no filters. → FAISS. It is unbeatable for offline dedup, clustering, and batch ANN, and it’s what the other tools are measured against.
- You already run Postgres and have up to a few million vectors. → pgvector. The transactional consistency, SQL filtering, and “no new database” simplicity beat a marginal latency difference. Start here and most teams should stay.
- You want zero infrastructure and can pay monthly, and you expect aggressive scaling. → Pinecone. Accept the black box and the lock-in in exchange for not owning a cluster.
- You want a purpose-built, filter-heavy vector database, self-hostable, and you’re willing to run one more service. → Qdrant. It’s the strongest open-source middle ground when pgvector’s performance isn’t enough and you don’t want a proprietary black box.
Summary
- FAISS, pgvector, Pinecone, and Qdrant are not competitors on the same feature — they sit on a spectrum from in-process library to managed SaaS, and the choice is mostly about operations and filtering, not raw search speed.
- FAISS is the fastest search kernel but you build everything around it: persistence, filtering, scaling.
- pgvector puts search inside the Postgres you already run, with native SQL filtering and transactions — the lowest-friction choice for most teams.
- Pinecone removes operations entirely for a recurring cost and a black box you can’t tune or escape easily.
- Qdrant offers a purpose-built, filter-aware, self-hostable database — the best open-source middle ground.
Measure recall, not just latency. Tune your efSearch/nprobe. And pick the tool whose responsibility model matches the team and budget you actually have — the fastest index you can’t operate is slower than a modest one you can.
Comments
Leave a comment