Site icon NexGismo

pgvector vs MongoDB Atlas Vector Search for Node.js RAG

pgvector vs MongoDB Atlas Vector Search for Node.js RAG
TL;DR
  • What: pgvector adds vector search inside PostgreSQL; MongoDB Atlas Vector Search adds it to MongoDB through a separate search index.
  • Why it matters: The choice decides how filters, consistency and hybrid search behave in your Node.js RAG app.
  • What to do: Keep vectors in the database that already holds your source of truth, then tune filtering for that engine.
  • Key stat: pgvector’s HNSW index covers vector columns up to 2,000 dimensions; Atlas accepts up to 8,192.

pgvector vs MongoDB Atlas Vector Search comes down to where your data already lives. Pick pgvector when PostgreSQL is your system of record and you need transactional, read-your-writes vector queries. Pick Atlas Vector Search when your app is already on MongoDB and you want indexed pre-filters, higher dimension limits and built-in hybrid search.

pgvector is an open-source PostgreSQL extension that adds vector data types, distance operators and approximate nearest-neighbour indexes (HNSW and IVFFlat). MongoDB Atlas Vector Search is MongoDB’s vector search feature, queried through the $vectorSearch aggregation stage and backed by a separate search index kept in sync with the collection.

The pgvector vs MongoDB Atlas Vector Search question usually shows up halfway through a RAG build. You have embeddings, a Node.js API and a tenant filter, and the first “top 10” query returns three results. Most comparisons online are vendor pages or benchmarks that pit a whole database against another on unrelated workloads. They rarely explain the two behaviours that actually bite Node.js teams: when filters run and when a new row becomes searchable. This guide compares both engines on those points using only their official documentation. It includes runnable Node.js code for each, a decision table, and a small calculation we ran in the sandbox to show how filter selectivity hurts pgvector’s defaults. If you are caching LLM answers on top of retrieval, read our semantic caching for LLMs guide after this one.

What is the core difference between pgvector and MongoDB Atlas Vector Search?

The core difference is architecture: pgvector stores and indexes vectors inside PostgreSQL itself, while Atlas Vector Search indexes them in a separate search process. That one fact explains most of the trade-offs below.

pgvector is an ordinary Postgres extension. It installs on Postgres 13 and newer, and its index lives in the same storage and write-ahead log as your tables. The README confirms that pgvector uses the WAL, so replication and point-in-time recovery work as they do for any other index.

Atlas Vector Search works differently. A process called mongot watches change streams and keeps its own copy of the indexed fields. MongoDB’s docs state plainly that this makes vector search indexes eventually consistent. A document you just inserted may not appear in $vectorSearch results straight away.

My recommendation: decide first whether your feature needs read-your-writes semantics. A chat app that embeds a user’s uploaded file and immediately asks questions about it does. A nightly-ingested knowledge base does not.

How do you run a pgvector vs MongoDB Atlas Vector Search query in Node.js?

Both engines need a few lines of Node.js: pgvector uses plain SQL through any Postgres client, and Atlas uses an aggregation pipeline with $vectorSearch as the first stage.

pgvector with node-postgres

The official pgvector-node package registers the types and serialises arrays. It also supports Prisma, Drizzle, Kysely and other ORMs.

import pg from 'pg';
import pgvector from 'pgvector/pg';

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  onConnect: async (client) => pgvector.registerTypes(client),
});

// one-time setup
await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
await pool.query(`CREATE TABLE IF NOT EXISTS chunks (
  id bigserial PRIMARY KEY,
  tenant_id text NOT NULL,
  body text NOT NULL,
  embedding vector(1536)
)`);
await pool.query(
  'CREATE INDEX IF NOT EXISTS chunks_hnsw ON chunks USING hnsw (embedding vector_cosine_ops)'
);

export async function searchPg(tenantId, queryEmbedding, k = 10) {
  const { rows } = await pool.query(
    `SELECT id, body, embedding <=> $1 AS distance
       FROM chunks
      WHERE tenant_id = $2
      ORDER BY embedding <=> $1
      LIMIT $3`,
    [pgvector.toSql(queryEmbedding), tenantId, k]
  );
  return rows;
}

Atlas Vector Search with the MongoDB driver

Atlas needs a vector search index that declares the vector field and every field you want to pre-filter on. The Node.js driver guide shows the same pattern.

import { MongoClient, Binary } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);
const chunks = client.db('rag').collection('chunks');

// one-time setup: vector field + filter field in ONE index
await chunks.createSearchIndex({
  name: 'chunks_vec',
  type: 'vectorSearch',
  definition: {
    fields: [
      { type: 'vector', path: 'embedding', numDimensions: 1536, similarity: 'cosine' },
      { type: 'filter', path: 'tenantId' },
    ],
  },
});

export async function searchAtlas(tenantId, queryEmbedding, k = 10) {
  return chunks.aggregate([
    {
      $vectorSearch: {
        index: 'chunks_vec',
        path: 'embedding',
        queryVector: Binary.fromFloat32Array(Float32Array.from(queryEmbedding)),
        filter: { tenantId },
        numCandidates: k * 20,
        limit: k,
      },
    },
    { $project: { body: 1, score: { $meta: 'vectorSearchScore' } } },
  ]).toArray();
}

Binary.fromFloat32Array stores the embedding as a compact BSON vector instead of an array of doubles. MongoDB’s index docs recommend BSON BinData vectors for efficient storage. If you generate embeddings with a structured LLM pipeline, our LLM structured outputs in Node.js guide covers validating the model output before it reaches either database.

Why does pgvector return fewer results than LIMIT when you filter?

pgvector applies WHERE filters after the approximate index scan, so a selective filter can discard most of the candidates the index found. This is the most common pgvector surprise in multi-tenant RAG apps, and most tutorials skip it.

The pgvector README spells out the arithmetic. With HNSW and the default hnsw.ef_search of 40, a condition that matches 10% of rows returns only about 4 rows on average. Your LIMIT 10 becomes a silent “up to 4”.

We ran a tiny script in the sandbox (Node.js v22.23.4) to extend that documented example to other selectivities. It simply multiplies the default candidate list of 40 by the share of rows that match:

selectivity=50% expected_rows=20.0
selectivity=10% expected_rows=4.0
selectivity=2%  expected_rows=0.8
selectivity=1%  expected_rows=0.4

With a hundred tenants of equal size, each tenant is about 1% of rows. A naive filtered query then expects less than one result. These are expected values from the documented default, not measured latencies.

How to fix pgvector filtering

  1. Upgrade to pgvector 0.8.0 or later and enable iterative scans, which keep scanning the index until enough rows match: SET hnsw.iterative_scan = strict_order;
  2. Raise the candidate list per transaction for selective queries: SET LOCAL hnsw.ef_search = 100;
  3. For a few distinct filter values, create partial indexes per value.
  4. For many tenants, use partitioning, as the pgvector filtering section suggests.
// Node.js: scope the setting to one transaction
const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query("SET LOCAL hnsw.iterative_scan = 'strict_order'");
  const { rows } = await client.query(
    'SELECT id, body FROM chunks WHERE tenant_id = $1 ORDER BY embedding <=> $2 LIMIT 10',
    [tenantId, pgvector.toSql(queryEmbedding)]
  );
  await client.query('COMMIT');
  return rows;
} finally {
  client.release();
}

Atlas avoids this specific trap by design. Fields declared as type: 'filter' in the index are used for pre-filtering before the vector search, and the docs call out multi-tenant scoping as the main use case. The catch: every field you filter on must be declared in the index definition. MongoDB also warns that filtered queries are typically slower than unfiltered ones, and that overly narrow pre-filters can exclude semantically relevant documents.

Which handles larger embeddings: pgvector or Atlas Vector Search?

Atlas Vector Search accepts larger embeddings out of the box, while pgvector needs half-precision or quantisation above 2,000 dimensions for an indexed column.

A pgvector vector column can store up to 16,000 dimensions, but HNSW indexes only cover vector up to 2,000 dimensions. The README lists halfvec up to 4,000 and bit up to 64,000 dimensions as the escape hatches. Atlas sets a single ceiling: numDimensions must be 8,192 or fewer.

Storage matters too. pgvector documents that each vector takes 4 × dimensions + 8 bytes, and each half vector takes 2 × dimensions + 8 bytes. Our sandbox script applied those formulas:

dims=768  vector=3080B  halfvec=1544B
dims=1024 vector=4104B  halfvec=2056B
dims=1536 vector=6152B  halfvec=3080B
dims=3072 vector=12296B halfvec=6152B

So a 3,072-dimension embedding stored as halfvec costs the same bytes as a 1,536-dimension vector, and becomes indexable. Atlas offers the equivalent through its quantization option, which accepts none, scalar or binary.

How do you tune recall in pgvector vs MongoDB Atlas Vector Search?

Both engines use HNSW graphs and expose similar knobs: one set at build time, one at query time.

In pgvector, the build-time options are m (default 16) and ef_construction (default 64). At query time you raise hnsw.ef_search from its default of 40. Index builds are much faster when the graph fits in maintenance_work_mem.

In Atlas, the build-time options are maxEdges, between 16 and 64 with a default of 16, and numEdgeCandidates, between 100 and 3,200 with a default of 100. At query time you set numCandidates. It must be 10,000 or less, and MongoDB recommends at least 20 times your limit.

MongoDB’s docs also give a useful target: good ANN recall is typically 90–95% overlap with exact search. Atlas exposes exact search directly through exact: true. In pgvector, you get exact results by querying without an index, which makes measuring recall on your own data easy in both engines.

My recommendation: build an evaluation set from real user questions, run exact and approximate search side by side, and tune until you hit that overlap range.

pgvector vs MongoDB Atlas Vector Search: which is better for hybrid search?

Atlas has a native hybrid search operator, while pgvector relies on you combining it with PostgreSQL full-text search in SQL.

MongoDB’s $rankFusion stage merges a $vectorSearch pipeline and a $search pipeline using reciprocal rank fusion. It requires MongoDB 8.0 or higher, and its RRF formula uses a sensitivity constant of 60 that MongoDB chose. You still need a separate Atlas Search index for the keyword side.

pgvector’s README points you to Postgres full-text search for hybrid search. The pgvector-node repo includes a hybrid search example. You write the fusion yourself, typically two CTEs and an RRF score in SQL. That is more code, but it runs in one transaction and one round trip.

If your stack is MEAN or MERN and you already run Atlas Search, $rankFusion is the shorter path. If you are on Postgres, a hand-written RRF query is well understood and avoids a second system.

When should you choose pgvector, and when Atlas Vector Search?

In the pgvector vs MongoDB Atlas Vector Search choice, pick the engine that matches your primary database; switching databases just for vector search rarely pays off. The table below summarises the documented differences.

pgvector vs MongoDB Atlas Vector Search: documented differences (sources linked in each row’s section above)
Factor pgvector Atlas Vector Search
Where the index lives Inside Postgres, WAL-logged Separate mongot index
Consistency Transactional with your rows Eventually consistent
Max indexed dimensions 2,000 (vector), 4,000 (halfvec) 8,192
Filtering Post-filter; iterative scans since 0.8.0 Pre-filter on indexed filter fields
HNSW defaults m 16, ef_construction 64, ef_search 40 maxEdges 16, numEdgeCandidates 100
Hybrid search DIY with Postgres full-text + RRF $rankFusion on MongoDB 8.0+
Node.js tooling pgvector npm package; Drizzle, Prisma, Kysely support Official mongodb driver, BSON vectors

Pick pgvector when Postgres is your system of record, you need joins with relational data, or a user must search content they uploaded a second ago. Pick Atlas when your documents already live in MongoDB, you filter heavily by tenant or category, or you want built-in hybrid ranking. If you run on AWS and also cache prompts, our Bedrock prompt caching guide pairs well with either retrieval layer.

The NexGismo verdict: For most Node.js teams, the right vector database is the one you already run. I’d use pgvector for Postgres-backed SaaS apps, but only with iterative scans switched on from day one, because the default post-filter behaviour silently shrinks results. I’d use Atlas Vector Search for MERN apps with heavy tenant filtering, and design for its eventual consistency instead of fighting it.

Frequently Asked Questions

Is pgvector better than MongoDB Atlas Vector Search?

Neither is better in general. pgvector suits apps where PostgreSQL is the system of record and vector results must be transactional with other rows. Atlas Vector Search suits apps already on MongoDB that need indexed pre-filtering and native hybrid search. Migrating your primary database only to get vector search usually costs more than it saves, so start with the engine you already run.

Can MongoDB be used as a vector database?

Yes. MongoDB stores embeddings in documents and queries them with the $vectorSearch aggregation stage, backed by a vector search index. It supports approximate and exact nearest-neighbour search, pre-filtering on indexed fields and automatic quantisation. It runs on Atlas clusters and also on self-managed or local Atlas CLI deployments, according to MongoDB’s documentation.

How many dimensions can pgvector index?

pgvector’s HNSW and IVFFlat indexes support the vector type up to 2,000 dimensions, halfvec up to 4,000 dimensions and bit up to 64,000 dimensions. A vector column can store up to 16,000 dimensions without an index. For larger embeddings, the pgvector README suggests half-precision indexing, binary quantisation, subvector indexing or dimensionality reduction.

Why does pgvector return fewer results than my LIMIT?

pgvector applies WHERE conditions after the approximate index scan. With HNSW’s default ef_search of 40, a filter matching 10% of rows returns about 4 rows on average. Enable iterative index scans in pgvector 0.8.0 or later, raise hnsw.ef_search, or use partial indexes and partitioning for selective filters.

Can I use MongoDB vector search without Atlas?

Yes. MongoDB’s $vectorSearch documentation states that vector search also works with self-managed deployments and with local Atlas deployments created through the Atlas CLI. Check the deployment options page for your MongoDB version before committing, because feature availability can differ between Atlas and self-managed installs.

The pgvector vs MongoDB Atlas Vector Search decision is less about raw speed and more about two behaviours: when filters apply and when new data becomes searchable. pgvector gives you transactional vectors inside Postgres but needs iterative scans for filtered queries. Atlas gives you indexed pre-filters, larger dimensions and native hybrid search, at the cost of eventual consistency. Run both code samples above against a copy of your real data, measure recall against exact search, and let those numbers decide. If you want more Node.js and AI engineering deep dives like this one, browse the latest posts on NexGismo.

Sources

Exit mobile version