Django Integration¶
django-pgsync is the official Django integration for PGSync. Declare search indexes on your Django models, and PGSync keeps Elasticsearch/OpenSearch in sync with your database. You never write the schema JSON by hand.
pip install django-pgsync # includes pgsync
pip install "django-pgsync[celery]" # with Celery beat support
Requires Python 3.10+, Django 4.2+ and PostgreSQL.
Why not signals?¶
Indexers built on Django signals hook into the ORM, so anything that bypasses
Model.save() silently never reaches your index. django-pgsync uses
PGSync's change data capture, which watches the database instead:
| Write path | Signal indexers | django-pgsync |
|---|---|---|
instance.save() / delete() |
||
queryset.update() / delete() |
||
bulk_create() / bulk_update() |
||
| Cascade deletes | ||
| Raw SQL / data migrations | ||
| Writes from other services |
Quick start¶
1. Register the app¶
INSTALLED_APPS = [
# ...
"django_pgsync",
]
PGSYNC = {
"MODE": "polling", # "polling" (default) | "event" | "wal"
"ELASTICSEARCH_URL": "http://localhost:9200",
}
Database credentials are read from DATABASES["default"] automatically.
Every other PGSYNC key is passed through to PGSync as an
environment setting.
2. Declare an index¶
from django_pgsync import PGSyncIndex, Nested
from .models import Author, Book, Publisher, Rating
class BookIndex(PGSyncIndex):
model = Book
index = "books"
fields = ["isbn", "title", "description"]
children = [
Nested(Rating, fields=["value"], label="ratings"), # one_to_many
Nested(Publisher, fields=["name"], label="publisher"), # one_to_one
Nested(Author, fields=["name"], label="authors"), # M2M through
]
Relationships are inferred from model metadata: foreign keys, one-to-one fields, and many-to-many through tables. Each committed change lands in the index as a denormalized nested document:
{
"isbn": "9780441172719",
"title": "Dune",
"description": "Politics, religion and giant sandworms.",
"authors": [{"name": "Frank Herbert"}],
"ratings": [{"value": 5}, {"value": 5}, {"value": 4}],
"publisher": {"name": "Chilton Books"}
}
3. Bootstrap and run¶
python manage.py pgsync_schema # inspect generated schema JSON
python manage.py pgsync_bootstrap # one-time setup for the mode
python manage.py pgsync_pull # one-shot sync, then exit
python manage.py pgsync_status # verify rows == documents
python manage.py pgsync_daemon # continuous sync (systemd etc.)
Run modes¶
| Mode | How it works | Postgres requirements |
|---|---|---|
polling (default) |
Periodic forward pass | None beyond read access; works on read-only and managed clusters |
event |
Triggers + pg_notify + replication slot |
wal_level=logical, slot rights, triggers |
wal |
Streams the logical replication slot | wal_level=logical, slot rights |
Why polling is the default
Polling does not need superuser access to change database settings, so
it suits hosted Postgres (RDS, Cloud SQL, Supabase) where you may not
control wal_level. When you control the database, wal gives real
time sync with the lowest overhead.
Root deletes in polling mode
Deleting a root row leaves a stale document in the index, because there is
no delete record to observe. Deletes on child rows are fine, because
PGSync rebuilds the parent document. If you delete root rows
permanently, use wal/event mode, or mark rows as deleted with a flag.
Celery beat¶
Schedule periodic sync without a dedicated daemon process:
CELERY_BEAT_SCHEDULE = {
"pgsync-pull": {
"task": "django_pgsync.tasks.pgsync_pull",
"schedule": 15.0,
},
}
A cache lock prevents overlapping runs. Ticks that fire while a pull is still in flight simply skip, so short intervals are safe. With django-celery-beat the schedule is editable in the Django admin.
Learn more¶
- django-pgsync on GitHub: source, issues and a runnable example project
- django-pgsync on PyPI
- Schema definition: the PGSync schema that
pgsync_schemagenerates for you