For a small project, store your vectors in the database you already run: if that
is Postgres, add the pgvector extension and you are done. Reach for a dedicated
vector store only when scale, latency, or rich metadata filtering force the issue.
This guide explains what a vector database does, then gives you a
checkable way to choose one.
What does a vector database do?
A vector is the numeric fingerprint of a chunk of text. You produce one per chunk with an embedding model, then store it. At query time you embed the question into the same number space and ask: which stored vectors point in the same direction as this one? That single operation, nearest-neighbour search, is the whole job.
A vector database is the storage and search engine for that operation. It does three things a plain table does not:
- Indexes vectors for speed. Comparing a query against every row is exact but slow once you pass a few hundred thousand rows. A vector index trades a small amount of recall for a large drop in latency.
- Filters on metadata. Production queries are rarely "find similar text"
alone. They are "find similar text where
tenant_id = 42andpublished = true". The store applies those filters alongside the vector search. - Stays current. You insert, update, and delete vectors as your source data changes, without rebuilding everything.
If you are new to the surrounding pipeline (chunking, embedding, retrieving), read RAG Fundamentals first. This guide is about the one box in that pipeline labelled "vector store".
The one decision that drives the choice
Before comparing products, answer one question: do you already run a relational database for this project?
- Yes, and you have under a few million vectors. Use that database's vector
extension. For Postgres that is
pgvector. You get vector search, metadata filtering, transactions, and backups in the system you already operate. No new service, no new bill, no second source of truth to keep in sync. - No, or vectors are the centre of the product, or you are past a few million rows with tight latency targets. A dedicated vector store earns its keep.
Most small projects sit in the first bucket and never need to leave it. The common mistake is to start with a dedicated store "to be safe" and pay for operating a second database before there is a single user.
When should I use pgvector?
pgvector is an open-source extension that adds a vector column type and
nearest-neighbour operators to Postgres. You enable it once, store embeddings in a
normal column, and query with SQL.
You do not write any model-calling code to use it. You ask Claude to wire it up.
The query is ordinary SQL. pgvector gives you distance operators rather than a new query language:
| Operator | Meaning | Use when |
|---|---|---|
<=> | cosine distance | comparing meaning; the standard default for text |
<-> | L2 (Euclidean) distance | embeddings are normalised and you want straight-line distance |
<#> | negative inner product | your embedding model is tuned for dot-product scoring |
A nearest-neighbour query reads like any other ORDER BY ... LIMIT:
SELECT id, body
FROM documents
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 5;The WHERE clause is the point: vector search and your existing relational
filters run in one query, against one consistent copy of the data.Common gotchaA selective WHERE plus an HNSW index can quietly return fewer rows than your LIMIT, because the graph walk visits a fixed candidate pool first and your filter then discards most of it.Do not assume an empty result means no matches; widen the search pool (raise hnsw.ef_search) or add a plain B-tree index on the filtered column so Postgres can pre-filter when the predicate is very selective.
Which pgvector index should I use, HNSW or IVFFlat?
Without an index, that query scans every row. That is exact and fine for tens of thousands of rows. Past that, add an index. pgvector offers two, and the choice is checkable, not a matter of taste.
| Index | Build | Memory | Recall | Best when |
|---|---|---|---|---|
| HNSW | slower | higher | high with little tuning | data arrives over time; you want good recall by default |
| IVFFlat | faster | lower | needs tuning | you bulk-load data first, then build the index once |
HNSW builds a graph incrementally, so it works correctly even on a table that is still filling up, and it reaches high recall without much tuning. The cost is more memory and slower index builds. IVFFlat clusters vectors first, so it builds faster and uses less memory, but it needs representative data already in the table when you create the index. Build IVFFlat on an empty table and you get meaningless clusters and poor recall.
The matching index definitions, one per distance operator family:Common gotchaThe index operator class has to match the operator your query uses.Build with vector_cosine_ops but sort by <-> (L2) and Postgres silently ignores the index and falls back to a full scan, so it looks fast in testing and crawls in production. Pick one distance, then use its operator everywhere.
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);For a small project that grows gradually, start with HNSW and cosine distance. That combination gives good recall with the least tuning. Let Claude generate the migration and explain the tradeoff back to you so the decision is recorded:
When should I move to a dedicated vector store?
A dedicated vector database is a separate service whose entire design centres on nearest-neighbour search. You move to one when a concrete limit bites, not on a hunch. The signals:
- Scale. You are past roughly the low tens of millions of vectors and the Postgres index no longer fits comfortably in memory.
- Latency under load. You have a strict per-query budget at high concurrency and your Postgres instance cannot hold it while also serving its other traffic.
- Vectors are the product. Search relevance is the core feature, so you want built-in hybrid search, reranking hooks, and tuning knobs as first-class parts of the engine.
- Operational separation. You want vector load to scale independently from your transactional database.
If none of these is true, staying on pgvector is the cheaper and simpler
correct answer.
The shape of the field
The dedicated stores split along one axis first: managed (someone else runs it) versus self-hosted (you run it). Pick that axis before picking a name.
| Store | Hosting | Picked for |
|---|---|---|
| Pinecone | managed only | zero operational overhead; you never run the database |
| Qdrant | open source · managed cloud | strong performance-to-cost on self-hosted |
| Weaviate | open source · managed cloud | hybrid search as a built-in feature |
| Chroma | open source · embedded or server | simplicity for local development and prototypes |
| pgvector | runs inside Postgres | reusing the database you already operate |
Treat the "picked for" column as a starting hypothesis, then verify against your own data and query mix. Vendor benchmarks use vendor-friendly workloads. Yours is the only one that counts.
Connect a store to Claude through MCP
Once a store holds your data, you want Claude to query it during a coding or research session without you copying results around by hand. That is what the Model Context Protocol (MCP) is for. You run an MCP server that exposes the store's search as a tool, and Claude calls it. See MCP fundamentals for the full picture and Useful MCP servers for what already exists.
An MCP server is registered in a JSON config. A server that talks to a managed store over the network usually takes a connection URL and an API key as environment variables:
{
"mcpServers": {
"vector-store": {
"command": "npx",
"args": ["-y", "some-vector-store-mcp-server"],
"env": {
"VECTOR_STORE_URL": "https://your-instance.example.com",
"VECTOR_STORE_API_KEY": "your-key-here"
}
}
}
}The exact server name, arguments, and environment variables come from that server's own documentation, not from memory. Keep the API key in an environment variable rather than pasting it into the config file, and review what the server can do before you trust it: read MCP security basics first. A vector-store tool that can delete collections is one prompt away from deleting them.
With the server connected, you drive retrieval in plain language:
A safe path for a real project
You do not have to choose the final store on day one. Pick a path that lets you defer the expensive decision until you have data to make it with.
- Start on
pgvector. Add the extension to your existing Postgres, store embeddings, and add an HNSW cosine index. You now have working retrieval with no new service. - Keep the embedding step separate from storage. Whatever produces your vectors should not assume a specific store. Then moving stores is a change in one place, not a rewrite.
- Measure before you migrate. Track query latency at your real concurrency and watch index memory. Migrate only when a number, not a worry, crosses a line you set in advance.
- When you migrate, re-evaluate retrieval. A new store changes the index and sometimes the distance metric. Re-run your retrieval evaluation so a regression shows up as a number. RAG Fundamentals covers recall and precision; check both before and after.
Ask Claude to hold you to this rather than reaching for a heavier tool early:
What to remember
- A vector database is storage plus nearest-neighbour search plus metadata filtering. That is the whole job.
- If you already run Postgres and have under a few million vectors, use
pgvector. It is the cheapest correct answer for a small project. - Default to an HNSW index with cosine distance for data that grows over time; use IVFFlat only when you bulk-load first and want faster, lighter builds.
- Move to a dedicated store when a concrete limit (scale, latency, or store-centric features) bites, and decide managed versus self-hosted before picking a name.
- Connect whichever store you choose to Claude through MCP so retrieval happens in your session, and lock down the server's permissions first.