Skip to content

paradedb/django-paradedb

ParadeDB

Simple, Elastic-quality search for Postgres

WebsiteDocsCommunityBlogChangelog


django-paradedb

PyPI Python Versions Downloads Codecov License Slack URL X URL

The official Python client for ParadeDB — Elastic-quality full-text, similarity, and hybrid search inside Postgres — built for the Django ORM.

Features

  • 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 custom Agg)
  • 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 Q objects, filter(), exclude(), and custom managers
  • Diagnostic management commands for index health and verification
  • Type-aware with a py.typed package marker and typed public APIs

Requirements & Compatibility

Component Supported
Python 3.10+
Django 4.2+
ParadeDB 0.22.0+
PostgreSQL 15+ (with ParadeDB extension)

Notes:

  • CI runs Python 3.10 through 3.14 across Django 4.2, 5.2, and 6.0.
  • Schema compatibility is verified against each new ParadeDB release.

Installation

pip install django-paradedb

or with uv:

uv add django-paradedb

Quick Start

Prerequisites

This guide assumes you have installed pg_search, and have configured your Django project with the Postgres database where pg_search is installed.

Create an Index

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 migrate

The json_fields option enables native dotted-path access for JSON subfields such as metadata.color in facets and aggregations.

Index Computed Expressions

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.

Generate Test Data

To demonstrate search, we need to populate the table we just created. First, open a Python shell:

python manage.py shell

And 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()

Text Search

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 ORM Compatibility

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
)

Custom Manager

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()

Diagnostics Helpers and Commands

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 search

Notes:

  • Management commands are discovered by Django only when "paradedb" is in INSTALLED_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_search versions.

Common Errors

"facets() requires a ParadeDB search condition in the WHERE clause"

# ❌ 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')

"facets(include_rows=True) requires order_by() and a LIMIT"

# ❌ 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)

Security

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.

Examples

Documentation

Contributing

See CONTRIBUTING.md for development setup, running tests, linting, and the PR workflow.

Support

If you're missing a feature or have found a bug, please open a GitHub Issue.

To get community support, you can:

If you need commercial support, please contact the ParadeDB team.

Acknowledgments

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

License

django-paradedb is licensed under the MIT License.

About

Official extension to Django for use with ParadeDB

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Contributors