Skip to the solution
HardSystem Design19 min read

pgvector vs Pinecone vs Qdrant: how do you choose a vector DB in 2025?

379 views

The problem

Question presented to candidate: "We run Postgres today and are adding RAG over customer documents, with thousands of tenants. The team is split between pgvector, Pinecone and Qdrant. How would you make that decision, and what would you test before committing?"

What a strong answer should cover:

  • The three are different shapes: pgvector is an extension inside the Postgres you already run (vectors next to rows, SQL filters, transactions); Pinecone is a fully managed service billed on usage; Qdrant is an open-source engine you self-host or buy as a managed cloud, with rich payload filtering and quantization
  • Filtered search is the real differentiator: post-filtering a small ANN candidate list returns too few results for selective filters; know pgvector ef_search and the iterative index scans added in 0.8.0, Qdrant payload indexes and filter-aware HNSW with a query planner, and Pinecone filtering integrated into retrieval
  • Multi-tenancy design per system: Postgres row-level security, partitions or partial indexes; Pinecone namespaces; Qdrant tenant payload indexes and per-tenant graphs
  • Scale and cost levers: dimension limits (pgvector HNSW vector up to 2,000, halfvec up to 4,000), memory and quantization, managed usage pricing versus servers you run
  • Operations and consistency: one database and one backup versus a second system to sync; who is on call
  • A proof of concept on your own data: recall@k against exact search, p95 latency at target QPS with real filters, ingest and delete behaviour, cost at projected scale

Clarifying questions expected:

  • How many vectors and tenants, how uneven are tenant sizes, and how fast does it grow?
  • What latency and query rate must we meet, and how selective are the typical filters?
  • Do we have the people to operate another stateful system, and are there data-residency rules?

Code / implementation expected: Optional โ€” explaining or sketching the filtered query (for example the pgvector SQL with an HNSW index and iterative scan) and a recall measurement is a strong signal.

  • vectordb
  • pgvector

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

19 min read ยท 14 sections

Read the code

Filtered top-10 search: post-filter vs iterative scan vs pre-filter vs partition
// Filtered vector search: four strategies and what each returns for "top 10
// WHERE <filter>" as the filter gets more selective. The "index" here returns
// candidates in exact nearest-first order, EF at a time: a best-case ANN index,
// so every loss below comes from the filter strategy, not from approximation.
let seed = 2025;
const rand = () => { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; };
const D = 16, N = 6000, K = 10, EF = 40, MAX_SCAN = 2000, QUERIES = 10;

const pts = [];
for (let i = 0; i < N; i++) { const v = []; for (let d = 0; d < D; d++) v.push(rand() - 0.5); pts.push(v); }
const FILTERS = [
  ["50% of rows", (i) => i % 2 === 0],
  ["10% of rows", (i) => i % 10 === 3],
  ["1% of rows", (i) => i % 100 === 7],
  ["0.2% of rows", (i) => i % 500 === 7],
];
const dist2 = (a, b) => { let s = 0; for (let d = 0; d < D; d++) { const t = a[d] - b[d]; s += t * t; } return s; };
const qs = [];
for (let j = 0; j < QUERIES; j++) { const v = []; for (let d = 0; d < D; d++) v.push(rand() - 0.5); qs.push(v); }

function strategies(q, match) {
  const all = pts.map((p, i) => [dist2(q, p), i]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  const truth = all.filter(([, i]) => match(i)).slice(0, K).map((x) => x[1]);
  const out = {};
  // post-filter: take EF nearest candidates, then apply WHERE
  out.post = { ids: all.slice(0, EF).filter(([, i]) => match(i)).slice(0, K).map((x) => x[1]), touched: EF };
  // iterative scan: keep pulling EF more until K matches or MAX_SCAN candidates seen
  let seen = 0; const got = [];
  while (got.length < K && seen < MAX_SCAN && seen < N) {
    for (const [, i] of all.slice(seen, seen + EF)) if (match(i) && got.length < K) got.push(i);
    seen += EF;
  }
  out.iter = { ids: got, touched: seen };
  // pre-filter: find matching rows first (a B-tree or payload index), then exact distance on them
  const subset = all.filter(([, i]) => match(i));
  out.pre = { ids: subset.slice(0, K).map((x) => x[1]), touched: subset.length };
  // partition: a separate index per filter value (namespace, partial index, per-tenant graph)
  out.part = { ids: subset.slice(0, K).map((x) => x[1]), touched: Math.min(EF, subset.length) };
  return { truth, out };
}

console.log(`${N} vectors, top ${K}, EF ${EF}, iterative cap ${MAX_SCAN}, ${QUERIES} queries`);
console.log("filter        strategy      avg returned  recall   avg vectors touched");
const NAMES = { post: "post-filter", iter: "iterative", pre: "pre-filter", part: "partition" };
for (const [label, match] of FILTERS) {
  const acc = {};
  for (const k of Object.keys(NAMES)) acc[k] = { ret: 0, hit: 0, touched: 0 };
  for (const q of qs) {
    const { truth, out } = strategies(q, match);
    const t = new Set(truth);
    for (const k of Object.keys(NAMES)) {
      acc[k].ret += out[k].ids.length; acc[k].touched += out[k].touched;
      acc[k].hit += out[k].ids.filter((i) => t.has(i)).length;
    }
  }
  for (const k of Object.keys(NAMES)) {
    const a = acc[k];
    console.log(`${label.padEnd(14)}${NAMES[k].padEnd(14)}${(a.ret / QUERIES).toFixed(1).padEnd(14)}${((100 * a.hit) / (QUERIES * K)).toFixed(0).padStart(3)}%    ${(a.touched / QUERIES).toFixed(1)}`);
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 77 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

Fine-tune vs RAG vs prompt engineering: how do you choose in 2025-26?

Continue