| name | documentdb-builder |
|---|---|
| description | Use this skill when building applications on top of DocumentDB — the open-source, MongoDB-compatible document database engine built on PostgreSQL. Covers spinning up a cluster, CRUD, indexing, aggregation, joins, the gateway, and best practices. Trigger when a user asks to build, query, or operate a DocumentDB instance, write application code against it, or understand how to structure data and queries. |
DocumentDB is the engine powering Azure DocumentDB. It is a fully open-source (MIT license), document-oriented NoSQL database engine built natively on PostgreSQL. It exposes a MongoDB-compatible API while storing BSON documents inside a PostgreSQL framework.
Key capabilities:
- CRUD operations on BSON documents
- Full-text search
- Geospatial queries
- Vector embeddings (RAG, similarity search)
- Aggregation pipelines
- Cross-collection joins via
$lookup - Background indexing (single-field, compound, TTL)
Two core components:
| Component | Role |
|---|---|
pg_documentdb_core |
PostgreSQL extension — adds BSON datatype and operations |
pg_documentdb |
Public API surface — CRUD, indexing, aggregation, collection management |
Third component (optional):
| Component | Role |
|---|---|
pg_documentdb_gw |
Gateway — protocol translation layer; accepts MongoDB wire protocol, maps to PostgreSQL |
Your App (MongoDB driver / mongosh)
│
▼
pg_documentdb_gw ← Accepts MongoDB wire protocol, SCRAM auth, TLS
(DocumentDB Gateway) ← Translates to PostgreSQL operations
│
▼
pg_documentdb ← Public API: CRUD, indexes, aggregation, collections
│
▼
pg_documentdb_core ← BSON datatype support inside PostgreSQL
│
▼
PostgreSQL ← Storage, WAL, reliability
You can interact via:
- MongoDB client / mongosh → through the Gateway (port
10260) - psql / SQL client → direct PostgreSQL shell (port
9712), usingdocumentdb_api.*functions
This gives you a full MongoDB-compatible endpoint with no build step.
docker run -dt \
-p 10260:10260 \
-e USERNAME=<username> \
-e PASSWORD=<password> \
ghcr.io/microsoft/documentdb/documentdb-local:latestConnect with mongosh:
mongosh localhost:10260 \
-u <username> -p <password> \
--authenticationMechanism SCRAM-SHA-256 \
--tls \
--tlsAllowInvalidCertificatesThe cleanest way to run DocumentDB alongside your app in a sample. Create a
docker-compose.yml at the root of your project:
version: "3.8"
services:
documentdb:
image: ghcr.io/microsoft/documentdb/documentdb-local:latest
ports:
- "10260:10260"
environment:
- USERNAME=docdbuser
- PASSWORD=Admin100!
restart: unless-stopped
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- documentdb
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stoppedStart everything together:
docker compose up -dStop and remove containers:
docker compose downIf your sample is app-only (DocumentDB runs separately), include just the documentdb
service and omit the app service. Your connection string should then point to
host.docker.internal:10260 from inside other containers, or localhost:10260 from
the host machine.
# Pull image (Ubuntu 22.04, PostgreSQL 16, AMD64, version 0.103.0)
docker pull mcr.microsoft.com/cosmosdb/ubuntu/documentdb-oss:22.04-PG16-AMD64-0.103.0
# Run container
docker run -dt mcr.microsoft.com/cosmosdb/ubuntu/documentdb-oss:22.04-PG16-AMD64-0.103.0
# Shell into container
docker exec -it <container-id> bash
# Connect to psql
psql -p 9712 -d postgresFor external access (exposes port to host):
docker run -p 127.0.0.1:9712:9712 -dt \
mcr.microsoft.com/cosmosdb/ubuntu/documentdb-oss:22.04-PG16-AMD64-0.103.0 -eExternal psql connection:
psql -h localhost --port 9712 -d postgres -U documentdbIf you want to build and run DocumentDB from source (instead of using Docker), follow these steps. This guide is designed for beginners and works best on Ubuntu/Debian. For other operating systems, package names may differ.
Prerequisites
Recommended: use the provided devcontainer for VSCode which contains all the dependencies pre-installed.
Or install the required dependencies manually:
sudo apt update
sudo apt install build-essential libbson-dev postgresql-server-dev-all pkg-config rustc cargoStep 1: Build PostgreSQL Extensions
sudo make installStep 2: Build the Gateway
scripts/build_and_install_with_pgrx.sh -i -d pg_documentdb_gw_host/Step 3: Start PostgreSQL and the Gateway
scripts/start_oss_server.sh -c -gStep 4: Connect and Test
Using a MongoDB client:
mongosh --host localhost --port 10260 --tls --tlsAllowInvalidCertificates \
-u docdbuser -p Admin100!Using the PostgreSQL shell:
psql -p 9712 -d postgres# Build packages for Debian 12, PostgreSQL 16
./packaging/build_packages.sh --os deb12 --pg 16
# Output lands in ./packages/ by defaultThe emulator supports two modes on container startup.
Unless disabled, these collections are created in the sampledb database:
users(5 records)products(5 records)orders(4 records)analytics(metrics + activity data)
docker run -p 10260:10260 -p 9712:9712 \
--skip-init-data \
--password mypassword \
documentdb/localdocker run -p 10260:10260 -p 9712:9712 \
-v /path/to/your/scripts:/init_doc_db.d \
--init-data-path /init_doc_db.d \
--password mypassword \
documentdb/localScripts in the directory run in alphabetical order via mongosh.
| Variable | Default | Description |
|---|---|---|
INIT_DATA_PATH |
/init_doc_db.d |
Directory containing .js init files |
SKIP_INIT_DATA |
false |
Set true to skip built-in sample data |
Production note: Always set
SKIP_INIT_DATA=trueand provide vetted scripts viaINIT_DATA_PATHin production deployments.
All DocumentDB operations use the documentdb_api schema. Every function takes a
database name and a JSON command as arguments.
-- Insert one document
SELECT documentdb_api.insert_one(
'mydb',
'users',
'{"name": "Alice", "age": 30, "email": "alice@example.com"}'
);
-- Insert multiple documents
SELECT documentdb_api.insert(
'mydb',
'{"insert": "users", "documents": [
{"name": "Bob", "age": 25},
{"name": "Carol", "age": 35}
]}'
);-- Find all documents in a collection
SELECT document FROM documentdb_api.collection('mydb', 'users');
-- Find with filter
SELECT document FROM documentdb_api.collection('mydb', 'users')
WHERE document @@ '{"age": {"$gt": 28}}';-- Update one document
SELECT documentdb_api.update(
'mydb',
'{"update": "users",
"updates": [{"q": {"name": "Alice"}, "u": {"$set": {"age": 31}}}]
}'
);-- Delete one document
SELECT documentdb_api.delete(
'mydb',
'{"delete": "users",
"deletes": [{"q": {"name": "Bob"}, "limit": 1}]
}'
);SELECT * FROM documentdb_api.list_collections_cursor_first_page(
'mydb',
'{"listCollections": 1}'
);SELECT documentdb_api.list_indexes_cursor_first_page(
'mydb',
'{"listIndexes": "users"}'
);TTL indexes are scheduled via pg_cron. Inspect them with:
SELECT * FROM cron.job;Indexing uses documentdb_api.create_indexes_background — runs without blocking
database operations.
SELECT * FROM documentdb_api.create_indexes_background(
'mydb',
'{"createIndexes": "users",
"indexes": [{"key": {"age": 1}, "name": "idx_age"}]
}'
);SELECT * FROM documentdb_api.create_indexes_background(
'mydb',
'{"createIndexes": "users",
"indexes": [{"key": {"country": 1, "age": 1}, "name": "idx_country_age"}]
}'
);CALL documentdb_api.drop_indexes(
'mydb',
'{"dropIndexes": "users", "index": "idx_age"}'
);- Create indexes on fields used in
$match,$lookupjoin keys, and sort stages. - Use compound indexes when queries consistently filter on multiple fields together.
- Avoid over-indexing — each index adds write overhead.
- Prefer background creation (
create_indexes_background) in production to avoid locking. - Review index usage regularly and drop unused indexes.
DocumentDB supports MongoDB-compatible aggregation pipelines via
documentdb_api.aggregate_cursor_first_page.
SELECT cursorpage FROM documentdb_api.aggregate_cursor_first_page(
'mydb',
'{"aggregate": "orders",
"pipeline": [
{"$group": {"_id": "$status", "total": {"$count": {}}}}
],
"cursor": {"batchSize": 10}
}'
);SELECT cursorpage FROM documentdb_api.aggregate_cursor_first_page(
'mydb',
'{"aggregate": "users",
"pipeline": [
{"$bucket": {
"groupBy": "$age",
"boundaries": [20, 30, 40, 50],
"default": "other"
}}
],
"cursor": {"batchSize": 10}
}'
);SELECT cursorpage FROM documentdb_api.aggregate_cursor_first_page(
'mydb',
'{"aggregate": "orders",
"pipeline": [
{"$group": {
"_id": "$customer_id",
"products": {"$addToSet": "$product_name"}
}}
],
"cursor": {"batchSize": 10}
}'
);| Stage | Purpose |
|---|---|
$match |
Filter documents |
$group |
Group + reduce |
$project |
Shape output fields |
$sort |
Order results |
$limit |
Cap result size |
$unwind |
Flatten arrays |
$lookup |
Join from another collection |
$bucket |
Bucket documents by range |
$addToSet |
Collect unique values |
DocumentDB supports MongoDB-style $lookup for joining data across collections.
-- First, populate appointment data
SELECT documentdb_api.insert_one('mydb', 'appointments',
'{"appt_id": "A001", "patient_id": "P001", "doctor": "Dr. Smith", "date": "2024-01-20"}'
);
-- Join: each patient with their appointments
SELECT cursorpage FROM documentdb_api.aggregate_cursor_first_page(
'mydb',
'{"aggregate": "patients",
"pipeline": [
{"$lookup": {
"from": "appointments",
"localField": "patient_id",
"foreignField": "patient_id",
"as": "appointments"
}},
{"$unwind": "$appointments"},
{"$project": {"_id": 0, "name": 1, "appointments.doctor": 1, "appointments.date": 1}}
],
"cursor": {"batchSize": 10}
}'
);Best practice: Create an index on the foreignField of the joined collection to
avoid full scans on the right-hand side of the join.
The Gateway is a protocol translation layer: it accepts MongoDB wire protocol from any MongoDB-compatible client and maps it to PostgreSQL operations.
| Capability | Detail |
|---|---|
| Protocol translation | MongoDB wire protocol → PostgreSQL operations |
| Authentication | SCRAM-SHA-256, mapped to PostgreSQL credentials |
| Transactions | BEGIN/COMMIT/ROLLBACK via TransactionStore |
| Cursor paging | cursorId-based paging with getMore |
| User management | createUser, updateUser, dropUser → PostgreSQL roles |
| TLS termination | Built-in |
# Build
docker build . -f .github/containers/Build-Ubuntu/Dockerfile_gateway -t documentdb-gw
# Run (exposes port 10260)
docker run -dt -p 10260:10260 \
-e USERNAME=<username> \
-e PASSWORD=<password> \
documentdb-gw
# Connect via mongosh
mongosh localhost:10260 -u <username> -p <password> \
--authenticationMechanism SCRAM-SHA-256 \
--tls \
--tlsAllowInvalidCertificatespg_documentdb_gw/
├── documentdb_gateway/ # Binary entry point (Tokio runtime, TLS init)
├── documentdb_gateway_core/ # TCP wire protocol + PostgreSQL bridge
├── documentdb_macros/ # Proc-macro crate
└── documentdb_tests/ # End-to-end tests
├── commands/ # Per-command test validators
├── test_setup/ # Test client creation, gateway lifecycle
└── utils/ # Shared test utilities
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:10260', {
auth: { username: 'myuser', password: 'mypassword' },
tls: true,
tlsAllowInvalidCertificates: true, // dev only
});
await client.connect();
const db = client.db('mydb');
const users = db.collection('users');
// Insert
await users.insertOne({ name: 'Alice', age: 30 });
// Find
const result = await users.find({ age: { $gt: 25 } }).toArray();
// Update
await users.updateOne({ name: 'Alice' }, { $set: { age: 31 } });
// Delete
await users.deleteOne({ name: 'Alice' });from pymongo import MongoClient
client = MongoClient(
'mongodb://localhost:10260',
username='myuser',
password='mypassword',
tls=True,
tlsAllowInvalidCertificates=True # dev only
)
db = client['mydb']
users = db['users']
# Insert
users.insert_one({'name': 'Alice', 'age': 30})
# Find
for doc in users.find({'age': {'$gt': 25}}):
print(doc)
# Update
users.update_one({'name': 'Alice'}, {'$set': {'age': 31}})
# Delete
users.delete_one({'name': 'Alice'})Always read the connection URI from an environment variable and exit with a clear
message if it is missing. Only enable tlsAllowInvalidCertificates when an explicit
env flag is set — do not default it to True, as that encourages insecure defaults
if the code is reused against a real deployment.
The connection string itself must always include tls=true and
tlsAllowInvalidCertificates=true (for the local container) as query parameters so
the intent is visible and auditable at the call site.
import os
import sys
from pymongo import MongoClient
def get_client() -> MongoClient:
uri = os.getenv("DOCUMENTDB_URI")
if not uri:
sys.exit(
"Error: DOCUMENTDB_URI environment variable is not set.\n"
"Copy .env.example to .env and fill in your connection string."
)
# tlsAllowInvalidCertificates is only safe for the local dev container.
# Never enable this against a real deployment — use a valid certificate instead.
allow_invalid_certs = os.getenv("DOCUMENTDB_ALLOW_INVALID_CERTS", "false").lower() == "true"
return MongoClient(uri, tlsAllowInvalidCertificates=allow_invalid_certs).env.example for the local container (sets the flag explicitly):
DOCUMENTDB_URI=mongodb://<username>:<password>@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true&authMechanism=SCRAM-SHA-256
DOCUMENTDB_ALLOW_INVALID_CERTS=true
For a real deployment, omit DOCUMENTDB_ALLOW_INVALID_CERTS (it defaults to false)
and use a connection string without tlsAllowInvalidCertificates=true.
# Local
psql -p 9712 -d postgres
# External
psql -h localhost --port 9712 -d postgres -U documentdbDocumentDB supports native vector indexing alongside document data — no separate vector store required.
-- Create a collection with vector embeddings
SELECT documentdb_api.insert_one('mydb', 'articles',
'{"title": "Intro to RAG",
"content": "Retrieval Augmented Generation...",
"embedding": [0.12, 0.87, 0.34, ...]
}'
);
-- Create a vector index
SELECT * FROM documentdb_api.create_indexes_background(
'mydb',
'{"createIndexes": "articles",
"indexes": [{
"key": {"embedding": "cosmosSearch"},
"name": "idx_vector",
"cosmosSearchOptions": {
"kind": "vector-ivf",
"numLists": 1,
"similarity": "COS",
"dimensions": 1536
}
}]
}'
);LangChain and Semantic Kernel both support MongoDB-compatible vector stores and work with DocumentDB through the Gateway without code changes.
Set via the similarity field in cosmosSearchOptions. Choose based on how your
embedding model was trained:
| Metric | Value | When to use |
|---|---|---|
| Cosine similarity | COS |
Default choice for most text and multimodal embeddings. Measures the angle between vectors, ignoring magnitude. Use when vectors may not be normalised. |
| Euclidean distance | L2 |
Use when absolute distance in vector space matters — e.g. image embeddings or models explicitly trained with L2 loss. |
| Inner product | IP |
Use only when vectors are already unit-normalised (magnitude = 1). Equivalent to cosine in that case but faster. Incorrect results if vectors are not normalised. |
When in doubt, use COS. Most popular embedding models (nomic-embed-text,
text-embedding-ada-002, mxbai-embed-large) are trained for cosine similarity.
numLists controls how many inverted index partitions IVF builds. The rule of thumb
is sqrt(n) where n is the number of documents in the collection.
| Dataset size | Recommended numLists |
|---|---|
| < 100 documents | 1 |
| ~1 000 documents | 32 |
| ~10 000 documents | 100 |
| ~100 000 documents | 316 |
| ~1 000 000 documents | 1000 |
Setting numLists too high relative to the dataset size degrades recall — the
query probes too few documents per list and misses neighbours. Setting it too low
reduces the benefit of the index. For development and small samples, always start
with numLists: 1.
Use the $search aggregation stage with cosmosSearch to run a vector similarity query.
returnStoredSource: true is required — without it the stage does not return the
source document fields, only internal metadata.
const pipeline = [
{
$search: {
cosmosSearch: {
vector: queryEmbedding, // number[] matching the index dimensions
path: 'embedding', // field that holds the stored vector
k: 5, // number of nearest neighbours to return
},
returnStoredSource: true, // REQUIRED — returns the full source document
},
},
// Split into two stages — see projection gotcha below
{ $addFields: { similarityScore: { $meta: 'searchScore' } } },
{ $project: { embedding: 0 } },
];
const results = await collection.aggregate(pipeline).toArray();DocumentDB does not allow a single $project stage to contain both an inclusion
(e.g. adding a computed field) and an exclusion (e.g. embedding: 0).
This will throw Cannot do exclusion on field embedding in inclusion projection:
// ❌ WRONG — mixes inclusion ({ $meta: ... }) and exclusion (0) in one stage
{ $project: { similarityScore: { $meta: 'searchScore' }, embedding: 0 } }Fix — use two separate stages:
// ✅ CORRECT
{ $addFields: { similarityScore: { $meta: 'searchScore' } } }, // add the score
{ $project: { embedding: 0 } }, // then excludeThis pattern applies any time you need both a computed/meta field and an excluded field in the same aggregation result.
-- Create a text index
SELECT * FROM documentdb_api.create_indexes_background(
'mydb',
'{"createIndexes": "articles",
"indexes": [{"key": {"content": "text"}, "name": "idx_fts"}]
}'
);
-- Full-text search query
SELECT document FROM documentdb_api.collection('mydb', 'articles')
WHERE document @@ '{"$text": {"$search": "retrieval augmented generation"}}';- Use consistent field names across documents in the same collection.
- Embed related data that is always read together; reference data that is accessed independently or is large.
- Avoid deeply nested arrays that require
$unwindon every query — flatten at insert time where possible.
- Index every field used in
$matchfilters and$lookupjoin keys. - Compound indexes follow left-prefix rules — order fields by selectivity (most selective first).
- Use
create_indexes_backgroundalways in production. - Audit indexes periodically: unused indexes add write cost with no read benefit.
- Always filter with
$matchas the first pipeline stage to reduce the working set early. - Use projection (
$project) to return only needed fields — reduces network and deserialization cost. - Paginate large result sets using
cursor.batchSizerather than fetching all documents at once.
- Use
SKIP_INIT_DATA=truein production — never ship with built-in sample data. - Provide vetted initialization scripts via
INIT_DATA_PATH. - The Gateway's
TransactionStoreauto-cleans expired transactions — no manual cleanup needed. - TTL indexes are scheduled via
pg_cron; verify cron jobs are running withSELECT * FROM cron.job.
- Use SCRAM-SHA-256 authentication through the Gateway for all application connections.
- Never set
tlsAllowInvalidCertificates=truein production. - Scope PostgreSQL roles to the minimum required permissions using the Gateway's role
system:
readAnyDatabase,readWriteAnyDatabase. - Disable built-in sample data and use
INIT_DATA_PATHwith your own scripts in production.
| Operation | Function / Command |
|---|---|
| Insert one | documentdb_api.insert_one(db, collection, doc) |
| Insert many | documentdb_api.insert(db, '{"insert": ..., "documents": [...]}') |
| Find (SQL) | SELECT document FROM documentdb_api.collection(db, collection) |
| Update | documentdb_api.update(db, '{"update": ..., "updates": [...]}') |
| Delete | documentdb_api.delete(db, '{"delete": ..., "deletes": [...]}') |
| Create index | documentdb_api.create_indexes_background(db, spec) |
| Drop index | documentdb_api.drop_indexes(db, spec) |
| List collections | documentdb_api.list_collections_cursor_first_page(db, spec) |
| List indexes | documentdb_api.list_indexes_cursor_first_page(db, spec) |
| Aggregate | documentdb_api.aggregate_cursor_first_page(db, pipeline) |
- GitHub: https://github.com/microsoft/documentdb
- Discord: https://aka.ms/documentdb_discord
- Docs: https://documentdb.io/docs
- Roadmap: https://github.com/orgs/microsoft/projects/1407/views/1
- File Issues: https://github.com/documentdb/documentdb/issues?q=is%3Aissue%20state%3Aopen%20label%3Adocumentdb-local
- FerretDB integration: https://github.com/FerretDB/FerretDB (uses DocumentDB as backend)
- License: MIT — https://opensource.org/license/mit
- Contributing: See
CONTRIBUTING.mdin the repo
| Issue | Fix |
|---|---|
make check fails with wrong ownership error on /tmp/data |
Drop /home/documentdb/code/pg_documentdb_core/src/test/regress/tmp/ and rerun make check |
| Build fails inside container | Run git config --global --add safe.directory /home/documentdb/code inside the container, then retry make |
| Can't connect externally via psql | Run container with -p 127.0.0.1:9712:9712 and the -e flag |
| mongosh connection refused | Verify Gateway is running on port 10260 and TLS flags are set |
| TTL index not expiring documents | Check SELECT * FROM cron.job — pg_cron must be running |