Simple, Elastic-quality search for Postgres
The official Python client for ParadeDB — Elastic-quality full-text, similarity, and hybrid search inside Postgres — built for the Django ORM.
- BM25 index management through Django migrations
- Full-text search with
Match,Term,FuzzyTerm,Regex,PhrasePrefix, and more - Faceted search and aggregations (
TopK,TopKWithCount,Percentile,Stats, and customAgg) - Relevance scoring with
Score()annotation - Hybrid search via Reciprocal Rank Fusion (RRF)
- More Like This queries for document similarity
- Autocomplete with prefix matching and fuzzy tolerance
- Composable with Django's
Qobjects,filter(),exclude(), and custom managers - Diagnostic management commands for index health and verification
- Type-aware with a
py.typedpackage marker and typed public APIs
| Component | Supported |
|---|---|
| Python | 3.10+ |
| Django | 4.2+ |
| ParadeDB | 0.22.0+ |
| PostgreSQL | 15+ (with ParadeDB extension) |
Notes:
- CI runs Python
3.10through3.14across Django4.2,5.2, and6.0. - Schema compatibility is verified against each new ParadeDB release.
pip install django-paradedbor with uv:
uv add django-paradedbThis guide assumes you have installed pg_search, and have configured your Django project with
the Postgres database where pg_search is installed.
Add a BM25 index to your model and use ParadeDBManager:
from django.db import models
from django.contrib.postgres.fields import IntegerRangeField
from paradedb.indexes import BM25Index
from paradedb.queryset import ParadeDBManager
class MockItem(models.Model):
description = models.TextField(null=True, blank=True)
rating = models.IntegerField(null=True, blank=True)
category = models.CharField(max_length=255, null=True, blank=True)
in_stock = models.BooleanField(null=True, blank=True)
metadata = models.JSONField(null=True, blank=True)
created_at = models.DateTimeField(null=True, blank=True)
last_updated_date = models.DateField(null=True, blank=True)
latest_available_time = models.TimeField(null=True, blank=True)
weight_range = IntegerRangeField(null=True, blank=True)
objects = ParadeDBManager()
class Meta:
db_table = "mock_items_django"
indexes = [
BM25Index(
fields={
"id": {},
"description": {"tokenizer": "unicode_words"},
"category": {"tokenizer": "literal"},
"rating": {},
"in_stock": {},
"metadata": {"json_fields": {"fast": True}},
"created_at": {},
"last_updated_date": {},
"latest_available_time": {},
"weight_range": {},
},
key_field="id",
name="search_idx",
),
]Run migrations to create the index:
python manage.py makemigrations
python manage.py migrateThe json_fields option enables native dotted-path access for JSON subfields
such as metadata.color in facets and aggregations.
You can index computed expressions using IndexExpression. This allows indexing
transformed values or combinations of fields:
from django.db.models import F
from django.db.models.functions import Lower
from paradedb.indexes import BM25Index, IndexExpression
class Article(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
views = models.IntegerField(default=0)
class Meta:
indexes = [
BM25Index(
fields={"id": {}, "title": {}, "body": {}},
expressions=[
# Text expression with tokenizer
IndexExpression(
Lower("title"),
alias="title_lower",
tokenizer="simple",
),
# Non-text expression with pdb.alias
IndexExpression(
F("views"),
alias="views_indexed",
),
],
key_field="id",
name="article_search_idx",
),
]For text expressions, specify a tokenizer. For non-text expressions (integers,
timestamps, etc.), omit the tokenizer to use pdb.alias.
To demonstrate search, we need to populate the table we just created. First, open a Python shell:
python manage.py shellAnd paste the following commands:
from django.db import connection
cursor = connection.cursor()
cursor.execute("""
CALL paradedb.create_bm25_test_table(
schema_name => 'public',
table_name => 'mock_items'
);
""")
cursor.execute("""
INSERT INTO public.mock_items_django
SELECT * FROM public.mock_items;
""")
cursor.close()Search with a simple query:
from paradedb.search import ParadeDB, Match, Term
# Single term
MockItem.objects.filter(description=ParadeDB(Match('shoes', operator='AND')))
# Multiple terms (explicit AND)
MockItem.objects.filter(description=ParadeDB(Match('running', 'shoes', operator='AND')))
# OR across terms
MockItem.objects.filter(description=ParadeDB(Match('shoes', 'boots', operator='OR')))
# Fuzzy search (typo tolerance via distance)
MockItem.objects.filter(description=ParadeDB(Match('shoez', operator='OR', distance=1)))
# Fuzzy prefix (distance + prefix matching)
MockItem.objects.filter(description=ParadeDB(Term('runn', distance=1, prefix=True)))
# Fuzzy transposition-cost-one
MockItem.objects.filter(description=ParadeDB(Term('shose', distance=1, transposition_cost_one=True)))Annotate with BM25 relevance score and sort by it:
from paradedb.functions import Score
MockItem.objects.filter(
description=ParadeDB(Match('shoes', operator='AND'))
).annotate(
score=Score()
).order_by('-score')django-paradedb works seamlessly with Django's ORM features:
from django.db.models import Q
from paradedb.search import ParadeDB, Match
# Combine with Q objects
MockItem.objects.filter(
Q(description=ParadeDB(Match('shoes', operator='AND'))) & Q(rating__gte=4)
)
# Chain with standard filters
MockItem.objects.filter(
description=ParadeDB(Match('shoes', operator='AND'))
).filter(
category='footwear'
).exclude(
rating__lt=4
)If you have a custom manager, compose it with ParadeDBQuerySet:
from paradedb.queryset import ParadeDBQuerySet
class CustomManager(models.Manager):
def active(self):
return self.filter(is_active=True)
CustomManagerWithParadeDB = CustomManager.from_queryset(ParadeDBQuerySet)
class MockItem(models.Model):
objects = CustomManagerWithParadeDB()django-paradedb includes helper functions for ParadeDB diagnostic table functions and
optional Django management commands:
paradedb_indexes()paradedb_index_segments()paradedb_verify_index()paradedb_verify_all_indexes()
Python helper example:
from paradedb.functions import paradedb_indexes, paradedb_verify_index
# Uses Django's default DB alias ("default")
rows = paradedb_indexes()
# Multi-DB: run against a specific database alias
checks = paradedb_verify_index("search_idx", using="search")Management command examples:
# Uses Django's default DB alias ("default")
python manage.py paradedb_indexes
# Multi-DB: target a specific database alias
python manage.py paradedb_verify_index search_idx --database searchNotes:
- Management commands are discovered by Django only when
"paradedb"is inINSTALLED_APPS. - The selected database must have ParadeDB (
pg_search) installed, and the target BM25 index must exist there. - Some diagnostics functions may not be available on older
pg_searchversions.
# ❌ Missing ParadeDB filter
MockItem.objects.filter(rating__lt=4).order_by('id')[:10].facets('category')
# ✅ Add a ParadeDB search filter
MockItem.objects.filter(
rating__gte=4,
description=ParadeDB(Match('shoes', operator='AND'))
).order_by('id')[:10].facets('category')# ❌ Missing ordering or limit
MockItem.objects.filter(description=ParadeDB(Match('shoes', operator='AND')))[:10].facets('category')
MockItem.objects.filter(description=ParadeDB(Match('shoes', operator='AND'))).order_by('id').facets('category')
# ✅ Both ordering and limit
MockItem.objects.filter(description=ParadeDB(Match('shoes', operator='AND'))).order_by('id')[:10].facets('category')
# ✅ Or skip rows entirely
MockItem.objects.filter(description=ParadeDB(Match('shoes', operator='AND'))).facets('category', include_rows=False)django-paradedb uses SQL literal escaping (rather than parameterized queries) for search terms. This is intentional: ParadeDB's full-text operators (&&&, |||, ===, @@@, etc.) require string literals that the query planner can inspect at parse time — parameterized placeholders are incompatible with this design. All user input is escaped via PostgreSQL's standard single-quote escaping (' → '') before being embedded in the query. The implementation is covered by 300+ tests including special-character and injection cases.
- Package Documentation: https://paradedb.github.io/django-paradedb
- ParadeDB Official Docs: https://docs.paradedb.com
- ParadeDB Website: https://paradedb.com
See CONTRIBUTING.md for development setup, running tests, linting, and the PR workflow.
If you're missing a feature or have found a bug, please open a GitHub Issue.
To get community support, you can:
- Post a question in the ParadeDB Slack Community
- Ask for help on our GitHub Discussions
If you need commercial support, please contact the ParadeDB team.
We would like to thank the following members of the Django community for their valuable feedback and reviews during the development of this package:
- Timothy Allen - Principal Engineer at The Wharton School, PSF and DSF member
- Frank Wiles - President & Founder of REVSYS
django-paradedb is licensed under the MIT License.