Vector Databases skill for Codex
Pick the store from numbers, then plan the re-embed. Codex starts from vector count, dimensions, filter shape and tenancy, estimates memory with and without quantization, and only then recommends pgvector, Qdrant, Pinecone, Weaviate, Chroma or Milvus. Collections get deterministic IDs, indexed payload filters and model version metadata, and embedding-model upgrades run through a new collection plus an alias flip instead of an outage.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- vector-databases Installs to ~/.agents/skills/vector-databases/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install vector-databases The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project vector-databases Installs to .agents/skills/vector-databases/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/vector-databases.zipunzip -o vector-databases.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) vector-databases PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $vector-databases, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Chooses, sizes and operates vector databases for embeddings - Qdrant, Pinecone, Weaviate, Chroma, Milvus or Postgres with pgvector - covering collection design, payload filtering, multi-tenancy, quantization, backups and zero-downtime re-embedding. Use when picking a vector store for RAG or semantic search, designing collections and metadata filters, migrating to a new embedding model, estimating memory and cost, or debugging slow or low-recall vector queries.
Use it when
- You are choosing a vector store for RAG or semantic search
- Collections need tenant isolation and metadata filters
- An embedding model change requires re-indexing everything
- Vector memory or managed-service cost is growing fast
Use something else when
- Vectors live in Postgres and you need index tuning: use pgvector-search
- You need lexical plus vector fusion and reranking: use hybrid-search-reranking
How to check the result
Recall@10 of ANN against exact search on 200 sampled queries is at least 0.95, a cross-tenant query returns nothing, and filtered p95 latency meets the target under load.
The mistake it prevents
Mixing vectors from two embedding models in one collection, or hard-coding a cosine threshold that silently breaks after a model upgrade.
Done well means: the store was chosen from measured requirements (vector count, dimensions, filter shape, tenancy, ops budget), collections carry the embedding model name and version, filters are indexed, recall is measured against exact search, and a re-embed can happen without downtime.
Decide before you install anything
Gather these numbers first; ask the user for any you cannot derive from the repo:
| Input | Why it matters |
|---|---|
| Vector count now and in 12 months | Memory and tier sizing |
| Dimensions and dtype (float32, float16, int8, binary) | count * dims * bytes is the raw floor |
| Filter shape (tenant, ACL, date, category) | Filtered ANN is where stores differ most |
| Tenancy model (one tenant per customer?) | Namespaces, partitions or payload filter |
| Write pattern (bulk nightly vs constant upserts) | Index rebuild cost, consistency |
| Hosting constraint (managed only, self-host, already run Postgres) | Ops budget |
| Need lexical/hybrid search in the same engine | Narrows the field |
Raw memory estimate: vectors * dims * 4 bytes for float32, then roughly 1.5x for HNSW graph
and overhead. 10M x 1536 floats is about 61 GB raw, so quantization or disk-backed indexes
stop being optional at that size.
Choice table
| Situation | Default | Reason |
|---|---|---|
| Already run Postgres, < ~10-20M vectors, relational filters and joins | pgvector ($pgvector-search) | One system, transactions, SQL filters, existing backups |
| Self-hosted, heavy filtering, hybrid dense+sparse, quantization | Qdrant | Payload indexes, server-side fusion, scalar/binary quantization, aliases |
| Fully managed, no ops team, serverless pricing | Pinecone (serverless) | Namespaces, no capacity planning; pay per read/write units |
| Want built-in BM25 hybrid, modules for vectorizing, GraphQL/gRPC API | Weaviate | Native hybrid query with alpha, multi-tenancy per collection |
| Prototype, notebook, local dev, tests | Chroma (embedded/persistent) | Zero setup; plan the production store separately |
| Billions of vectors, GPU indexes, dedicated platform team | Milvus / Zilliz | Built for that scale; heavier to run |
Default answer when nothing forces a choice: pgvector if Postgres is already in the stack, otherwise Qdrant. Do not add a second database for 200k vectors.
Workflow
- [ ] 1. Read AGENTS.md, docker-compose, IaC and existing clients to see what is already deployed
- [ ] 2. Fill the requirements table; estimate memory with and without quantization
- [ ] 3. Design the collection: vector config, payload schema, indexed filter fields, tenancy
- [ ] 4. Write an ingest path that is idempotent (deterministic IDs) and batched
- [ ] 5. Build a recall harness: ANN top-k vs exact top-k on 200 sampled queries
- [ ] 6. Load-test filtered queries at expected QPS; record p50/p95
- [ ] 7. Document backup/snapshot and re-embed procedures before go-live
Collection design rules
- One embedding model per collection (or per named vector). Store
embed_modelandembed_versionin collection metadata or a config table; never mix vectors from two models in one index, the distances are meaningless across models. - Distance metric matches the model. Most text embedding models (OpenAI, Cohere, BGE) expect cosine; with unit-normalised vectors dot product is equivalent and cheaper.
- Deterministic IDs:
uuid5(namespace, f"{doc_id}:{chunk_no}")so re-ingest upserts instead of duplicating. Qdrant requires UUID or unsigned int IDs. - Payload holds what you filter and cite:
tenant_id,doc_id,source_url,acl,updated_at,chunk_no. Keep full text in the payload only if you need it at query time; otherwise fetch from the primary DB bydoc_id. - Index every filter field. Unindexed payload filters force scans and wreck latency.
Qdrant example
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="kb_v2",
vectors_config={"dense": models.VectorParams(size=1536, distance=models.Distance.COSINE, on_disk=True)},
sparse_vectors_config={"sparse": models.SparseVectorParams(modifier=models.Modifier.IDF)},
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(type=models.ScalarType.INT8, always_ram=True)
),
)
client.create_payload_index("kb_v2", field_name="tenant_id",
field_schema=models.KeywordIndexParams(type="keyword", is_tenant=True))
client.create_payload_index("kb_v2", field_name="updated_at", field_schema="datetime")
client.update_collection_aliases(change_aliases_operations=[
models.CreateAliasOperation(create_alias=models.CreateAlias(collection_name="kb_v2", alias_name="kb"))
])
hits = client.query_points(
collection_name="kb", # always query the alias
query=query_vec, using="dense", limit=10,
query_filter=models.Filter(must=[
models.FieldCondition(key="tenant_id", match=models.MatchValue(value=tenant)),
]),
search_params=models.SearchParams(hnsw_ef=128, quantization=models.QuantizationSearchParams(rescore=True)),
).points
Pinecone example
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone() # PINECONE_API_KEY; needs network
if not pc.has_index("kb-v2"):
pc.create_index("kb-v2", dimension=1536, metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"))
index = pc.Index("kb-v2")
index.upsert(vectors=[{"id": cid, "values": vec, "metadata": meta}], namespace=tenant)
res = index.query(vector=q, top_k=10, namespace=tenant, include_metadata=True,
filter={"doc_type": {"$in": ["guide", "faq"]}})
Dimension and metric are fixed at index creation. Namespaces are the tenant boundary; a query never crosses namespaces, which is exactly what you want for isolation.
Multi-tenancy
| Tenants | Pattern |
|---|---|
| Few, large, strict isolation | Separate collections or indexes |
| Many small (thousands+) | Single collection with an indexed tenant_id (Qdrant is_tenant), Pinecone namespaces, Weaviate multi-tenancy |
| Mixed | Large tenants get their own collection; the long tail shares one |
The tenant filter is a security control. Enforce it in one server-side function that every query goes through, and write a test that a query for tenant A never returns tenant B data.
Quantization and memory
| Mode | Memory vs float32 | Recall impact | Use |
|---|---|---|---|
| float16 / halfvec | 50% | Negligible | Almost always safe |
| Scalar int8 | ~25% | Small; rescoring recovers most | Default at > 5M vectors |
| Binary (1 bit) | ~3% | Large unless rescored with originals | High-dim models (>= 1024) with oversampling + rescore |
| Product quantization | Configurable | Model-dependent | Very large corpora; measure carefully |
Always keep original vectors (on disk) for rescoring and future index rebuilds.
Re-embedding without downtime
Embedding model upgrades are the most common painful migration. Plan for it on day one:
- Create a new collection
kb_v3with the new dimensions and metric. - Backfill from the source of truth (not from the old vector store), batched and resumable.
- Dual-write new and updated documents to both collections during the backfill.
- Run the recall harness and the retrieval eval set against
kb_v3. - Flip the alias (Qdrant aliases; Pinecone: switch the index name in config; pgvector: new column or table, then swap in a transaction).
- Keep
kb_v2for a rollback window, then delete it only after the user confirms.
Query embeddings must come from the same model as the collection. Ship the alias flip and the query-side model change together, behind one config value.
Operations
- Backups: Qdrant snapshots (
POST /collections/{name}/snapshots), Weaviate backup modules, Postgres base backups for pgvector, Pinecone collections or backups depending on plan. Restore one into a scratch environment before trusting it. - Deletes: deleting a document must delete all of its chunks (
filter doc_id = X). Test it; orphaned chunks keep surfacing deleted content. - Monitoring: p95 query latency split by filtered/unfiltered, segment or index count, memory, indexing queue length, and recall from the periodic harness.
- Destructive calls (
delete_collection,delete_index, dropping tables) require explicit user confirmation naming the collection and environment.
Gotchas
- Chroma’s defaults (L2 distance in older versions, embedded SQLite) are fine for prototypes; set the space explicitly to cosine and do not ship the embedded mode behind a multi-worker server.
- Highly selective filters on HNSW can return fewer than
kresults or degrade recall. Use payload indexes, filter-aware search (Qdrant, Weaviate), partitions per tenant, or raiseef. - Upsert batches that are too large time out; 100-500 vectors per request is a safe start.
- Cosine scores are not probabilities and shift between embedding models; never hard-code a similarity threshold without re-validating it after a model change.
- Managed-service network calls need network access; in a Codex sandbox use a local Docker
instance (
docker run -p 6333:6333 qdrant/qdrant) for tests when network is off.
Verification
# Recall@k of ANN vs exact search on sampled queries
def recall_at_k(ann_ids: list[list[str]], exact_ids: list[list[str]], k: int = 10) -> float:
return sum(len(set(a[:k]) & set(e[:k])) / k for a, e in zip(ann_ids, exact_ids)) / len(ann_ids)
- Exact search: Qdrant
SearchParams(exact=True), pgvector with index scans disabled, or brute-force NumPy over a sample. Target recall@10 >= 0.95 for RAG unless the user accepts less. - Tenant isolation test passes; delete-document test leaves zero chunks.
- Load test at expected QPS with real filters meets the latency target.
Report template
**Choice:** Qdrant 1.x self-hosted (reasons: 18M vectors, tenant filter + date filter, hybrid)
**Sizing:** 18M x 1024 float32 = 74 GB raw; int8 scalar in RAM 18 GB, originals on disk
**Collection:** kb_v3 (alias kb), cosine, payload indexes tenant_id (is_tenant), updated_at
**Recall@10 vs exact:** 0.97 at hnsw_ef=128, rescore on; p95 38 ms filtered at 200 QPS
**Runbooks written:** snapshot/restore, re-embed via alias flip, document delete What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Vector Databases"
short_description: "Pick the store from numbers, then plan the re-embed"
default_prompt: "$vector-databases Should we use Pinecone, Qdrant or pgvector for 15M chunks with per-customer filtering?" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.