vector-db — operate the store, not the embeddings
You own the store: the collection schema, the index, the filter path, recall-vs-latency
tuning, hybrid fusion, quantization, and the production knobs (upserts, namespaces, deletes,
backups). You operate it across the four engines a Claude agent actually meets: pgvector
(Postgres extension), Qdrant, Weaviate, Pinecone (serverless).
Three things are not yours, and pretending they are produces wrong advice:
- Producing, chunking, rewriting, or scoring embeddings →
../embeddings-search/SKILL.md. You
store vectors; you do not make or judge them.
- Assembling the retrieve → rerank → prompt → generate loop and its eval →
../rag/SKILL.md.
- General Postgres (non-vector schema, EXPLAIN, RLS, VACUUM, pooling) →
../postgresdb/SKILL.md.
You own only the pgvector surface: the vector/halfvec column, its index, its operators,
its recall. "My Postgres is slow in general" is theirs; "my <=> query has low recall" is yours.
Pick the engine
Match the engine to where the data already lives and how much ops you want to run. All four
implement HNSW with comparable recall at a matched ef, so the differentiator is operations and
hybrid, not raw quality.
| Engine |
Best when |
Hybrid built-in |
Ops cost |
Scale sweet spot |
| pgvector |
Data already in Postgres; one less system to run |
No — DIY (vector + tsvector, combine yourself) |
You already run Postgres |
≤ a few M vectors |
| Qdrant |
You want best filtered-search latency and self-host control |
Yes — query API, prefetch + RRF/DBSF, server-side IDF |
Self-host or cloud |
10M+ |
| Weaviate |
You want hybrid + modules out of the box |
Yes — alpha + fusionType, BlockMax WAND BM25 |
Self-host or cloud |
10M+ |
| Pinecone |
You refuse to operate anything |
Yes — sparse-dense, integrated inference |
Zero (serverless) |
Any (pay per use) |
Rule: don't add a new system to host vectors if the rows already live in Postgres and you are
under a few million. pgvector is one extension, not a second database to back up and monitor.
Design the collection & index
Distance metric MUST match the embedding model. A model trained for cosine, indexed with
L2, ranks silently wrong — no error, just bad results. OpenAI text-embedding-3-*, Cohere,
most sentence-transformers → cosine. pgvector operator cheatsheet:
<-> L2 / Euclidean (vector_l2_ops)
<=> cosine distance (vector_cosine_ops) <- the common one
<#> negative inner product (vector_ip_ops) <- for normalized vectors
Dimensions are fixed by the model, not a choice. text-embedding-3-small = 1536,
-3-large = 3072. pgvector's vector caps at 2000 dims for an index; for more, use halfvec.
HNSW defaults and the one invariant. Build-time m (default 16) and ef_construction
(default 64); query-time ef_search (pgvector default 40). Keep ef_construction >= 2*m
(so ≥32 at the default m) — too low starves graph quality and recall never recovers without
a rebuild. Raise m to 32–48 only for high-dim or high-recall needs (more RAM, slower build).
IVFFlat only when build speed beats recall. It is cheaper to build but lower recall and
needs lists/probes tuning; on a selective filter it is the wrong default (see next section).
Prefer HNSW unless you have a measured reason.
Named vectors when one object has multiple spaces (e.g. a dense semantic vector + a sparse
BM25 vector, or title-vector + body-vector). Qdrant and Weaviate support this natively; it is
how you do hybrid in one collection instead of two.
Metadata / payload filtering
The #1 "search is broken" bug: the filter is applied after top-k, so a selective filter
returns fewer than k rows (or zero). Fix it by filtering inside the search and indexing the
filter field.
Bad: ANN top-k=10, THEN drop rows where tenant_id != 'acme' -> often < 10, sometimes 0
Good: search the index WITH the filter as a constraint -> k rows that already match
Index every field you filter on. Unindexed filters force a scan and kill latency. Qdrant:
create a payload index. Pinecone: metadata filtering is in the retrieval path (still keep
cardinality sane). pgvector: a B-tree (or partition) on tenant_id so the planner can use it.
Prefer in-graph / in-path filtering. Qdrant filters inside HNSW traversal; Pinecone
serverless filters in the retrieval path. Both beat naive post-filter.
pgvector 0.8 iterative scan is the fix when a selective WHERE returns too few rows:
SET hnsw.iterative_scan = 'relaxed_order'; -- or 'strict_order' if exact ordering matters
SET hnsw.ef_search = 100;
SELECT id FROM docs
WHERE tenant_id = 'acme' -- selective filter
ORDER BY embedding <=> $1 -- cosine, matches the model
LIMIT 10;
Without iterative scan (pgvector < 0.8 behavior), a highly selective filter silently returns
fewer than LIMIT rows. Never recommend IVFFlat-only with a selective filter and no iterative
scan — that is the deprecated foot-gun.
Tune recall vs latency
You cannot tune what you do not measure. Establish recall before shipping.
Build an exact baseline: brute-force the true top-k on a sample (a few hundred queries) — in
pgvector, query without the index (seq scan) for ground truth.
Query the index and compute recall@k = overlap with the baseline.
Raise the query-time knob until recall hits target (commonly ≥0.95), then stop — higher ef
costs latency for nothing:
| Engine |
Knob |
Default |
| pgvector |
hnsw.ef_search |
40 |
| Qdrant |
hnsw_ef (search) |
per-collection |
| Weaviate |
ef (vectorIndexConfig) |
dynamic |
| Pinecone |
(managed) |
— |
Full parameter table and the recall recipe live in references/tuning.md.
Hybrid search
Dense (semantic) + sparse (BM25/keyword) catches exact terms, IDs, and rare tokens that dense
alone misses. The two normalize differently, so you fuse, you don't add raw scores.
- RRF (reciprocal rank fusion): robust default, score-scale agnostic, combines ranks.
- Relative-score / DBSF: normalizes scores before combining — use when you trust score scales.
Per engine:
- Weaviate: one call —
hybrid(query, alpha=0.5, fusionType=relativeScoreFusion). alpha
slides 0.0 (pure keyword) → 1.0 (pure vector). BM25 is BlockMax WAND (default from v1.30, ~10x faster).
- Qdrant:
prefetch a dense and a sparse query, then a fusion step (Fusion.RRF or DBSF);
IDF is computed server-side (v1.15+).
- Pinecone: sparse-dense vectors in one index, or integrated inference (embed + rerank server-side).
- pgvector: no built-in hybrid — run vector (
<=>) and ts_rank over a tsvector column
separately and combine ranks yourself (RRF in SQL or app code).
Concrete current-API code for all four is in references/engines.md.
Quantization & cost
Quantization trades recall for RAM/cost. Decide by dimension count and a recall test, never blind.
| Method |
Compression |
When safe |
| Scalar (int8) |
~4x |
Almost always; tiny recall loss. Good default RAM cut. |
| Product (PQ) |
8–64x |
Large corpora where RAM dominates; needs tuning + recall check. |
| Binary |
32x (40x faster via SIMD popcount) |
High-dim only (≥1024). On 384-dim it shreds recall — measure or don't. |
pgvector halfvec |
~2x |
Near-free: 16-bit float, near-identical recall, and required for >2000 dims. |
Reach for halfvec first in Postgres — it is the cheapest win. Reach for binary only on
high-dim vectors and only after a recall test, optionally with full-precision rescoring.
Operate it
- Batch upserts. One-by-one upserts are 10–100x slower and hammer the index. Send batches of
hundreds; size to the engine's payload limit.
- Namespaces / multitenancy. Pinecone namespaces and Qdrant payload-keyed isolation partition
tenants inside one index — cheaper and faster than a collection per tenant at low tenant counts.
- Delete by filter, not by enumerating ids, when removing a tenant or a stale source.
- Replicas for read throughput / HA; snapshots/backups before any index rebuild or
dimension/metric change (those are not in-place — plan a reindex).
- To "update" a vector, re-upsert by id. Do not store only raw text and re-embed on read.
Anti-patterns
| Anti-pattern |
Why it bites |
Do instead |
Cosine-trained model indexed with L2 (<->) |
Silently wrong ranking, no error |
Match metric to model — cosine → <=> / vector_cosine_ops |
| Post-filtering top-k results |
Returns < k rows, sometimes 0, on selective filters |
Filter inside the search; index the filter field |
| IVFFlat + selective filter, no iterative scan |
Drops rows; deprecated path in pgvector 0.8 |
HNSW + hnsw.iterative_scan='relaxed_order' |
| Never measuring recall |
"Search is bad" with no number to move |
Recall@k vs an exact baseline before shipping |
| Binary quantization on 384-dim |
Recall collapses, then blamed on the engine |
Binary only ≥1024 dims, after a recall test; else scalar/halfvec |
| One-by-one upserts |
10–100x slower, index thrash |
Batch hundreds per request |
ef_construction < 2*m |
Permanently weak graph; recall needs a full rebuild |
Keep ef_construction >= 2*m (≥32 at default m=16) |
| Store only raw text, re-embed to "update" |
Drift, cost, no point-update path |
Re-upsert the vector by id |
| Unindexed filter field |
Full scan, latency spikes |
Payload index (Qdrant) / B-tree (pgvector) / sane metadata cardinality (Pinecone) |
References & siblings
- references/engines.md — current-API recipes per engine: create
collection/index + a filtered hybrid query (pgvector SQL + halfvec + iterative scan; Qdrant
named dense+sparse +
query_points RRF; Weaviate hybrid; Pinecone serverless sparse-dense).
- references/tuning.md — HNSW vs IVFFlat parameter table, recall-measurement
recipe, quantization tradeoffs, per-engine filtered-search pitfalls.
Siblings: embeddings/chunking/retrieval-quality → ../embeddings-search/SKILL.md; the full RAG
loop → ../rag/SKILL.md; general Postgres → ../postgresdb/SKILL.md.
Validate a produced index DDL / collection schema with scripts/verify.sh <artifact-file>.
1---2name: vector-db3description: Use when operating a vector store as a data layer — choosing or migrating between Pinecone, Qdrant, Weaviate and pgvector; designing a collection or index (distance metric, dimensions, HNSW parameters, named vectors); filtering on metadata; hybrid dense-plus-sparse search; and quantization to cut RAM and cost. Covers garbage results, silently ignored filters, low recall, slow queries, and filtered queries returning fewer than k rows. NOT producing, chunking or judging embeddings (that is `embeddings-search`).4---56# vector-db — operate the store, not the embeddings78You own the **store**: the collection schema, the index, the filter path, recall-vs-latency9tuning, hybrid fusion, quantization, and the production knobs (upserts, namespaces, deletes,10backups). You operate it across the four engines a Claude agent actually meets: **pgvector**11(Postgres extension), **Qdrant**, **Weaviate**, **Pinecone** (serverless).1213Three things are **not** yours, and pretending they are produces wrong advice:1415- Producing, chunking, rewriting, or scoring embeddings → `../embeddings-search/SKILL.md`. You16 store vectors; you do not make or judge them.17- Assembling the retrieve → rerank → prompt → generate loop and its eval → `../rag/SKILL.md`.18- General Postgres (non-vector schema, EXPLAIN, RLS, VACUUM, pooling) → `../postgresdb/SKILL.md`.19 You own **only** the pgvector surface: the `vector`/`halfvec` column, its index, its operators,20 its recall. "My Postgres is slow in general" is theirs; "my `<=>` query has low recall" is yours.2122## Pick the engine2324Match the engine to where the data already lives and how much ops you want to run. All four25implement HNSW with comparable recall at a matched `ef`, so the differentiator is operations and26hybrid, not raw quality.2728| Engine | Best when | Hybrid built-in | Ops cost | Scale sweet spot |29|---|---|---|---|---|30| **pgvector** | Data already in Postgres; one less system to run | No — DIY (`vector` + `tsvector`, combine yourself) | You already run Postgres | ≤ a few M vectors |31| **Qdrant** | You want best filtered-search latency and self-host control | Yes — `query` API, `prefetch` + RRF/DBSF, server-side IDF | Self-host or cloud | 10M+ |32| **Weaviate** | You want hybrid + modules out of the box | Yes — `alpha` + `fusionType`, BlockMax WAND BM25 | Self-host or cloud | 10M+ |33| **Pinecone** | You refuse to operate anything | Yes — sparse-dense, integrated inference | Zero (serverless) | Any (pay per use) |3435Rule: **don't add a new system to host vectors if the rows already live in Postgres and you are36under a few million.** pgvector is one extension, not a second database to back up and monitor.3738## Design the collection & index39401. **Distance metric MUST match the embedding model.** A model trained for cosine, indexed with41 L2, ranks *silently wrong* — no error, just bad results. OpenAI `text-embedding-3-*`, Cohere,42 most sentence-transformers → cosine. pgvector operator cheatsheet:4344 ```text45 <-> L2 / Euclidean (vector_l2_ops)46 <=> cosine distance (vector_cosine_ops) <- the common one47 <#> negative inner product (vector_ip_ops) <- for normalized vectors48 ```49502. **Dimensions are fixed by the model**, not a choice. `text-embedding-3-small` = 1536,51 `-3-large` = 3072. pgvector's `vector` caps at 2000 dims for an index; for more, use `halfvec`.52533. **HNSW defaults and the one invariant.** Build-time `m` (default 16) and `ef_construction`54 (default 64); query-time `ef_search` (pgvector default 40). Keep `ef_construction >= 2*m`55 (so ≥32 at the default `m`) — too low starves graph quality and recall never recovers without56 a rebuild. Raise `m` to 32–48 only for high-dim or high-recall needs (more RAM, slower build).57584. **IVFFlat only when build speed beats recall.** It is cheaper to build but lower recall and59 needs `lists`/`probes` tuning; on a selective filter it is the wrong default (see next section).60 Prefer HNSW unless you have a measured reason.61625. **Named vectors when one object has multiple spaces** (e.g. a dense semantic vector + a sparse63 BM25 vector, or title-vector + body-vector). Qdrant and Weaviate support this natively; it is64 how you do hybrid in one collection instead of two.6566## Metadata / payload filtering6768The #1 "search is broken" bug: the filter is applied **after** top-k, so a selective filter69returns fewer than `k` rows (or zero). Fix it by filtering *inside* the search and indexing the70filter field.7172```text73Bad: ANN top-k=10, THEN drop rows where tenant_id != 'acme' -> often < 10, sometimes 074Good: search the index WITH the filter as a constraint -> k rows that already match75```7677- **Index every field you filter on.** Unindexed filters force a scan and kill latency. Qdrant:78 create a payload index. Pinecone: metadata filtering is in the retrieval path (still keep79 cardinality sane). pgvector: a B-tree (or partition) on `tenant_id` so the planner can use it.80- **Prefer in-graph / in-path filtering.** Qdrant filters *inside* HNSW traversal; Pinecone81 serverless filters in the retrieval path. Both beat naive post-filter.82- **pgvector 0.8 iterative scan** is the fix when a selective `WHERE` returns too few rows:8384 ```sql85 SET hnsw.iterative_scan = 'relaxed_order'; -- or 'strict_order' if exact ordering matters86 SET hnsw.ef_search = 100;87 SELECT id FROM docs88 WHERE tenant_id = 'acme' -- selective filter89 ORDER BY embedding <=> $1 -- cosine, matches the model90 LIMIT 10;91 ```9293 Without iterative scan (pgvector < 0.8 behavior), a highly selective filter silently returns94 fewer than `LIMIT` rows. Never recommend IVFFlat-only with a selective filter and no iterative95 scan — that is the deprecated foot-gun.9697## Tune recall vs latency9899You cannot tune what you do not measure. Establish recall **before** shipping.1001011. Build an exact baseline: brute-force the true top-k on a sample (a few hundred queries) — in102 pgvector, query without the index (seq scan) for ground truth.1032. Query the index and compute recall@k = overlap with the baseline.1043. Raise the query-time knob until recall hits target (commonly ≥0.95), then stop — higher `ef`105 costs latency for nothing:106107 | Engine | Knob | Default |108 |---|---|---|109 | pgvector | `hnsw.ef_search` | 40 |110 | Qdrant | `hnsw_ef` (search) | per-collection |111 | Weaviate | `ef` (vectorIndexConfig) | dynamic |112 | Pinecone | (managed) | — |113114Full parameter table and the recall recipe live in [references/tuning.md](references/tuning.md).115116## Hybrid search117118Dense (semantic) + sparse (BM25/keyword) catches exact terms, IDs, and rare tokens that dense119alone misses. The two normalize differently, so you **fuse**, you don't add raw scores.120121- **RRF** (reciprocal rank fusion): robust default, score-scale agnostic, combines ranks.122- **Relative-score / DBSF**: normalizes scores before combining — use when you trust score scales.123124Per engine:125126- **Weaviate**: one call — `hybrid(query, alpha=0.5, fusionType=relativeScoreFusion)`. `alpha`127 slides 0.0 (pure keyword) → 1.0 (pure vector). BM25 is BlockMax WAND (default from v1.30, ~10x faster).128- **Qdrant**: `prefetch` a dense and a sparse query, then a fusion step (`Fusion.RRF` or DBSF);129 IDF is computed server-side (v1.15+).130- **Pinecone**: sparse-dense vectors in one index, or integrated inference (embed + rerank server-side).131- **pgvector**: no built-in hybrid — run vector (`<=>`) and `ts_rank` over a `tsvector` column132 separately and combine ranks yourself (RRF in SQL or app code).133134Concrete current-API code for all four is in [references/engines.md](references/engines.md).135136## Quantization & cost137138Quantization trades recall for RAM/cost. Decide by **dimension count and a recall test**, never blind.139140| Method | Compression | When safe |141|---|---|---|142| Scalar (int8) | ~4x | Almost always; tiny recall loss. Good default RAM cut. |143| Product (PQ) | 8–64x | Large corpora where RAM dominates; needs tuning + recall check. |144| Binary | ~32x (~40x faster via SIMD popcount) | **High-dim only** (≥1024). On 384-dim it shreds recall — measure or don't. |145| pgvector `halfvec` | ~2x | Near-free: 16-bit float, near-identical recall, and required for >2000 dims. |146147Reach for `halfvec` first in Postgres — it is the cheapest win. Reach for binary only on148high-dim vectors and only after a recall test, optionally with full-precision rescoring.149150## Operate it151152- **Batch upserts.** One-by-one upserts are 10–100x slower and hammer the index. Send batches of153 hundreds; size to the engine's payload limit.154- **Namespaces / multitenancy.** Pinecone namespaces and Qdrant payload-keyed isolation partition155 tenants inside one index — cheaper and faster than a collection per tenant at low tenant counts.156- **Delete by filter**, not by enumerating ids, when removing a tenant or a stale source.157- **Replicas** for read throughput / HA; **snapshots/backups** before any index rebuild or158 dimension/metric change (those are not in-place — plan a reindex).159- **To "update" a vector, re-upsert by id.** Do not store only raw text and re-embed on read.160161## Anti-patterns162163| Anti-pattern | Why it bites | Do instead |164|---|---|---|165| Cosine-trained model indexed with L2 (`<->`) | Silently wrong ranking, no error | Match metric to model — cosine → `<=>` / `vector_cosine_ops` |166| Post-filtering top-k results | Returns < k rows, sometimes 0, on selective filters | Filter inside the search; index the filter field |167| IVFFlat + selective filter, no iterative scan | Drops rows; deprecated path in pgvector 0.8 | HNSW + `hnsw.iterative_scan='relaxed_order'` |168| Never measuring recall | "Search is bad" with no number to move | Recall@k vs an exact baseline before shipping |169| Binary quantization on 384-dim | Recall collapses, then blamed on the engine | Binary only ≥1024 dims, after a recall test; else scalar/halfvec |170| One-by-one upserts | 10–100x slower, index thrash | Batch hundreds per request |171| `ef_construction < 2*m` | Permanently weak graph; recall needs a full rebuild | Keep `ef_construction >= 2*m` (≥32 at default `m=16`) |172| Store only raw text, re-embed to "update" | Drift, cost, no point-update path | Re-upsert the vector by id |173| Unindexed filter field | Full scan, latency spikes | Payload index (Qdrant) / B-tree (pgvector) / sane metadata cardinality (Pinecone) |174175## References & siblings176177- [references/engines.md](references/engines.md) — current-API recipes per engine: create178 collection/index + a filtered hybrid query (pgvector SQL + halfvec + iterative scan; Qdrant179 named dense+sparse + `query_points` RRF; Weaviate `hybrid`; Pinecone serverless sparse-dense).180- [references/tuning.md](references/tuning.md) — HNSW vs IVFFlat parameter table, recall-measurement181 recipe, quantization tradeoffs, per-engine filtered-search pitfalls.182183Siblings: embeddings/chunking/retrieval-quality → `../embeddings-search/SKILL.md`; the full RAG184loop → `../rag/SKILL.md`; general Postgres → `../postgresdb/SKILL.md`.185186Validate a produced index DDL / collection schema with `scripts/verify.sh <artifact-file>`.