Read our operating thesis — Machines work better in tandem
JOURNALDATA · JULY 2026

Choosing the agent data layer: pgvector vs Chroma vs Pinecone

Start in Postgres. Here are the specific thresholds where that stops being true, why an agent's retrieval pattern argues for co-location, and why your recall problem is almost certainly chunking rather than the database.

WRITTEN BYTandem Machines
Coordinated Intelligence
PUBLISHED
READING TIME16 minutes
SHARE
LinkedInX
KEY TAKEAWAYS
  • 01If you already run Postgres, start with pgvector. It is the default, not the compromise.
  • 02Agents make many small retrievals inside a loop, so per-query overhead multiplies — co-location matters more than it does for search.
  • 03HNSW for query performance, IVFFlat when build time and memory bind. HNSW needs no training and works on an empty table.
  • 04Bad recall is a chunking problem far more often than a database problem. Changing stores does not fix it.
  • 05A second datastore is a second consistency problem: your rows and your embeddings can now disagree.

Start in Postgres

If you already run Postgres, use pgvector until you have a measured reason not to. It stores vectors in the same database as the relational data they describe, inside the same transactions, backups, replication and access control you already operate. That is not a starter option you graduate from — for most agent workloads it is the correct end state.

The case for this is not that pgvector wins a benchmark. It is that the alternative introduces a distributed system where you previously had one database, and that cost is paid continuously while the benefit only appears at scale you may never reach.

“A dedicated vector database is a scaling decision. Reaching for it first is an architecture decision you made without the information.”

pgvector is an open-source Postgres extension providing exact and approximate nearest-neighbour search with ACID compliance and point-in-time recovery. It handles the vector shapes real embedding models produce:

TypeLimitUse for
vector16,000 dimensionsStandard float embeddings
halfvec16,000 dimensionsHalf the storage; usually indistinguishable in recall
bit64,000 dimensionsBinary quantisation for very large indexes
sparsevec16,000 non-zero elementsSparse / keyword-style vectors for hybrid search
4 BYTES PER ELEMENT FOR VECTOR, 2 FOR HALFVEC — THE HALVING IS FREE ACCURACY-WISE FOR MOST MODELS

And the distance operators cover what any retrieval design needs: <-> for L2, <=> for cosine, <#> for negative inner product, <+> for L1, plus <~> and <%> for Hamming and Jaccard on binary vectors. If you are cosine-similarity shopping for a database, this row is the entire requirement and Postgres already meets it.

The agent access pattern is not search

A search product does one large retrieval per user request. An agent does many small retrievals inside a loop, each one sitting in the critical path of the next decision. That difference changes which costs matter: per-query overhead multiplies across the loop, and tail latency compounds rather than averaging out.

This is the argument most vector-database comparisons miss, because they are written for search. Consider the shapes:

Search productAgent loop
Retrievals per requestOne, sometimes twoFive to fifty
Latency toleranceA few hundred ms, onceEach one delays the next decision
Query shapeOne query, large result setMany narrow queries, small result sets
What dominatesIndex performance at scalePer-query fixed overhead

When a single retrieval carries a network round trip plus connection setup plus serialisation, and you do that thirty times in a loop, the fixed overhead is the cost — not the index. A local Postgres query on the same connection pool as the rest of your data avoids all of it. That is a real advantage of co-location and it is specific to agents.

The corollary: if your agent is doing thirty retrievals per run, the first optimisation is not a faster database. It is asking why thirty retrievals are necessary, which is usually a context-layer problem.

When Postgres stops being the answer

Four conditions justify a dedicated store: index size that no longer fits comfortably in memory on a machine you are willing to run, write throughput that competes with your transactional load, a hard requirement for capabilities Postgres lacks, or an operational preference not to tune an index yourself.
THE FOUR REASONS TO LEAVE POSTGRES
01Memory pressureThe index no longer fits comfortably in RAM you'll pay for
02Write contentionEmbedding writes compete with transactional load
03Missing capabilityMulti-modal, managed reranking, something genuinely absent
04Operational preferenceYou would rather not own index tuning at all
NOTE WHAT IS NOT ON THE LIST: 'WE ARE BUILDING WITH AI' AND 'IT FELT LIKE THE RIGHT TOOL'

On memory: HNSW is the fast index and it wants to be resident. The practical threshold is not a row count someone can quote you — it is the point where your index plus your working set exceeds the RAM you are willing to provision. Two mitigations come before migration: halfvec halves the storage at usually-imperceptible recall cost, and binary quantisation reduces index size substantially at higher scale.

On the fourth reason: it is legitimate and under-stated. “We do not want to own this” is a perfectly good basis for buying a managed service. It is just a different reason from the one people usually give, and being honest about which one applies leads to a better decision.

HNSW and IVFFlat

HNSW gives better query performance with slower builds and higher memory use, requires no training step, and can be created on an empty table. IVFFlat builds faster with lower memory and lower query performance, and must be created after the table has data because it requires training. For production query performance, use HNSW.

That last constraint on IVFFlat catches people in exactly one way: they create the index in a migration that runs before the data loads, get a useless index built on nothing, and never find out because the query still returns results — just poor ones.

Index creation
-- HNSW: the production default. No training step, safe on an empty table.
-- Defaults are m = 16, ef_construction = 64.
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- IVFFlat: faster to build, cheaper in memory, weaker at query time.
-- MUST be created after the data is loaded — it trains on existing rows.
-- lists = rows/1000 up to 1M rows; sqrt(rows) above that.
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 1000);   -- ≈ 1M rows
MATCH THE OPS CLASS TO YOUR DISTANCE OPERATOR — A COSINE INDEX DOES NOT SERVE AN L2 QUERY

The caption is its own small trap. The index operator class has to match the distance operator your query uses. A vector_cosine_ops index will not be used by a query written with <->, and Postgres will silently fall back to a sequential scan — fast enough in development, catastrophic in production. Check with EXPLAIN that your index is actually being hit.

Filtering is where recall quietly dies

Approximate search plus a selective metadata filter is the most common source of “it worked in testing and returns nothing in production”. If the filter is applied after the approximate search, the engine finds the nearest neighbours and then discards the ones that fail the filter — so a filter matching 1% of your corpus can leave you with nothing.

This matters far more for agents than for search, because agent queries are almost always scoped: this customer, this account, this document set, this date range. A scoped query is a filtered query, and filtered approximate search is the hard case in every vector engine.

Filter strategyBehaviourFailure mode
Post-filterRetrieve k nearest, then discard non-matchingSelective filters return few or zero results
Pre-filterRestrict the candidate set, then search within itCan degrade to a scan if the subset is large
PartitionedSeparate index per tenant or scopeIndex proliferation; per-partition overhead

In Postgres you have a lever the dedicated stores mostly do not: ordinary relational partitioning and B-tree indexes on the filter column, working alongside the vector index. For strongly-scoped access — one customer at a time, which describes most agent retrieval — a partitioned table makes the filtered case cheap rather than pathological.

Whatever you choose, test filtered recall explicitly at a realistic selectivity. A recall number measured on unfiltered queries tells you nothing about the query your agent will actually run.

Your recall problem is chunking

Retrieval quality is determined mostly by how documents were split, not by which engine stores them. Chunks that break mid-argument, lose their heading context, or combine unrelated sections produce embeddings that represent nothing precisely — and no database can retrieve a good answer from a bad chunk.

This is the most commonly misdiagnosed problem in the category. The symptom — “it retrieves irrelevant passages” — looks like a search problem, so teams evaluate search engines. The cause is usually upstream.

CHECK THESE BEFORE CHANGING DATABASES
01Heading contextDoes each chunk carry the section it came from?
02Semantic boundariesAre you splitting on structure, or on a character count?
03Chunk mixingDoes one chunk span two unrelated topics?
04Query symmetryDo your queries look like your chunks, or like questions?
ALL FOUR ARE CHEAPER TO FIX THAN A MIGRATION, AND MORE LIKELY TO BE THE CAUSE

The fourth is subtle and worth dwelling on. A question and the passage that answers it are not linguistically similar, so embedding both in the same space and hoping for proximity underperforms. Storing a generated question alongside each chunk, or embedding a summary rather than the raw text, often produces a bigger improvement than any index change — and costs one pass over your corpus rather than a migration.

The diagnostic
Take twenty real queries and read the top five results for each by hand. If the right passage is present but ranked fourth, you have a ranking problem. If it is absent, you have a chunking or embedding problem. Neither is solved by changing databases.

The three options, honestly

pgvector is a Postgres extension: co-located, transactional, and yours to operate. Chroma is open source with embedded, client-server and managed cloud deployment, and bundles embedding functions and search modes. Pinecone is a fully managed hosted service positioned for scale. They occupy different points on the same tradeoff.
pgvectorChromaPinecone
ShapePostgres extensionStandalone, embeddableManaged service
DeploymentYour PostgresIn-process, self-host, or cloudHosted
LicenceOpen sourceOpen source (Apache 2.0)Proprietary
Transactions with your dataYes — same databaseNoNo
Bundles embeddingNo — you call the modelYes — built-in embedding functionsConsult current docs
VERIFY OPERATIONAL DETAIL AND PRICING DIRECTLY — THIS CATEGORY CHANGES QUARTERLY

Chroma is the most pleasant of the three to start with, and the embedded mode is genuinely useful: no server, no network, works in a test. It bundles the things you would otherwise assemble — embedding functions for OpenAI, Cohere, Hugging Face and sentence-transformers, metadata filtering, keyword and regex search without embeddings, dense/sparse/hybrid querying, and multi-modal indexing across images, audio and text. If you need multi-modal retrieval, that is a real capability gap Postgres does not fill.

Pinecone positions itself as the vector database for AI agents and applications — semantic search, knowledge retrieval and long-term memory at scale, searching billions of items in milliseconds. Its operational specifics change often enough that quoting them here would be a disservice; if it is on your shortlist, the questions to take to its current documentation are filtered-search behaviour, per-query latency at your scale, and what happens to your data if you leave.

The cost nobody counts

A separate vector store means your rows and your embeddings can disagree. Every write now has two destinations with no shared transaction, so deletes, updates and failures produce drift — records whose embeddings are gone, and embeddings pointing at records that no longer exist.

This is the tax that never appears in a comparison table, and it is paid every day rather than at migration time. Four failure modes turn up reliably:

DriftCauseSymptom
Orphan embeddingRow deleted; vector delete failed or was never writtenAgent retrieves and cites a record that no longer exists
Stale embeddingRow updated; re-embedding deferred or droppedAgent acts on superseded content, confidently
Missing embeddingRow created; embedding job failed silentlyContent is invisible to retrieval with no error anywhere
Permission driftAccess control lives on the row, not the vectorRetrieval returns content the requester may not see

The last one is a security issue, not a data-quality issue, and it is the strongest single argument for co-location. When the vector lives in the same row as the record, your existing row-level access control covers it for free. When it lives in another system, you have to reimplement authorisation there — and a retrieval layer that returns content the requesting user could not otherwise read is a confused-deputy problem wearing a different hat. That connects directly to the agent threat model.

None of which makes a dedicated store wrong. It makes it a decision with a recurring cost that should be justified by a measured constraint. The order that works: start co-located, instrument retrieval quality and latency, fix your chunking, and migrate when a number tells you to — not when a landing page does.

Frequently asked questions

Do I need a vector database?

Probably not at first. If you already run Postgres, pgvector stores vectors next to the relational data they belong to, with the same transactions, backups and access control. A dedicated vector database earns its place at high scale, under heavy filtered-search load, or when you need capabilities Postgres does not have — not by default.

What is pgvector?

pgvector is an open-source Postgres extension for vector similarity search. It adds vector column types — vector and halfvec up to 16,000 dimensions, bit up to 64,000, and sparsevec — distance operators for L2, inner product, cosine, L1, Hamming and Jaccard, and two index types, HNSW and IVFFlat.

Should I use HNSW or IVFFlat in pgvector?

HNSW for production query performance: better speed-recall tradeoff, no training step, and it can be built on an empty table. IVFFlat when build time and memory are the binding constraints — it builds faster and uses less memory but returns lower query performance, and it must be created after the table has data because it requires training.

Why is my retrieval quality bad?

Almost always because of how the documents were split, not which database stores them. Chunks that break mid-argument, lose their heading context, or mix unrelated sections produce embeddings that represent nothing precisely. Changing databases does not fix that; changing the chunking usually does.

How does metadata filtering affect recall?

It depends on whether the filter is applied before or after the approximate search. Filtering after retrieval means the engine finds the nearest neighbours and then discards those that fail the filter, so a selective filter can leave you with almost nothing. This is a common cause of a search that works in testing and returns empty results in production.

Does the choice of vector store matter for agents specifically?

Yes, because agents make many small retrievals inside a loop rather than one large search per request, and each one sits in the critical path of the next decision. Per-query network overhead multiplies across the loop, which argues for co-locating vectors with the rest of your data until scale makes that impossible.

Sources

  1. 01pgvector: open-source vector similarity search for Postgres pgvector, 2026
  2. 02Chroma documentation Chroma, 2026
  3. 03Pinecone documentation Pinecone, 2026
Tandem Machines
Notes on coordinated intelligence — one essay a month
Subscribe
PREVIOUS

Semantic caching for AI agents