Skip to content

Commit 98230af

Browse files
committed
feat: support the paradedb index access method (paradedb/paradedb#5706)
1 parent 219541e commit 98230af

7 files changed

Lines changed: 290 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file. The format
66

77
## Unreleased
88

9+
## Unreleased
10+
11+
### Changed
12+
13+
- `BM25Index` now emits `USING paradedb` by default, the primary index access method name in pg_search 0.25.0+. Pass `am="bm25"` to keep the backwards-compatible `bm25` alias (required for servers on pg_search <= 0.24.x).
14+
915
## [0.10.0] - 2026-07-14
1016

1117
### Removed

examples/autocomplete/setup.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from pathlib import Path
1010

1111
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12+
from common import create_model_indexes
13+
1214
from models import AutocompleteItem
1315

1416

@@ -17,7 +19,7 @@ def setup_autocomplete_table() -> int:
1719
from django.db import connection
1820

1921
with connection.cursor() as cursor:
20-
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search")
22+
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search CASCADE")
2123

2224
# Ensure mock_items exists first
2325
cursor.execute(
@@ -57,16 +59,11 @@ def setup_autocomplete_table() -> int:
5759
print(f" ✓ Copied {count} products from mock_items")
5860

5961
print("\nCreating autocomplete-optimized BM25 index...")
60-
with connection.schema_editor(atomic=False) as schema_editor:
61-
for index in AutocompleteItem._meta.indexes:
62-
statement = index.create_sql(
63-
model=AutocompleteItem, schema_editor=schema_editor
64-
)
65-
schema_editor.execute(statement)
66-
print(" ✓ Created BM25 index with:")
67-
print(" - description (standard tokenizer)")
68-
print(" - description_ngram (ngram 3-8 for substring matching)")
69-
print(" - category (literal for exact matching)")
62+
create_model_indexes(AutocompleteItem)
63+
print(" ✓ Created BM25 index with:")
64+
print(" - description (standard tokenizer)")
65+
print(" - description_ngram (ngram 3-8 for substring matching)")
66+
print(" - category (literal for exact matching)")
7067

7168
return count
7269

examples/common.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,43 @@ def configure_django() -> None:
5959
django.setup()
6060

6161

62+
def server_index_am() -> str:
63+
"""Index access method to use: `paradedb` on pg_search 0.25.0+, else `bm25`."""
64+
from django.db import connection
65+
66+
with connection.cursor() as cursor:
67+
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search CASCADE")
68+
cursor.execute("SELECT 1 FROM pg_am WHERE amname = 'paradedb'")
69+
return "paradedb" if cursor.fetchone() else "bm25"
70+
71+
72+
def create_model_indexes(model) -> None:
73+
"""Create the model's declared indexes using the server's available AM."""
74+
from django.db import connection
75+
76+
am = server_index_am()
77+
with connection.schema_editor(atomic=False) as schema_editor:
78+
for index in model._meta.indexes:
79+
if isinstance(index, BM25Index) and index.am != am:
80+
_path, args, kwargs = index.deconstruct()
81+
index = BM25Index(*args, **{**kwargs, "am": am})
82+
statement = index.create_sql(model=model, schema_editor=schema_editor)
83+
schema_editor.execute(statement)
84+
85+
6286
def setup_mock_items() -> int:
6387
"""Create mock_items table with BM25 index. Returns row count."""
6488
from django.db import connection
6589

6690
with connection.cursor() as cursor:
67-
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search")
91+
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search CASCADE")
6892
cursor.execute(
6993
"CALL paradedb.create_bm25_test_table("
7094
"schema_name => 'public', table_name => 'mock_items')"
7195
)
7296
cursor.execute("DROP INDEX IF EXISTS mock_items_bm25_idx;")
7397

74-
# Render index SQL from the unmanaged model metadata.
75-
with connection.schema_editor(atomic=False) as schema_editor:
76-
for index in MockItem._meta.indexes:
77-
statement = index.create_sql(model=MockItem, schema_editor=schema_editor)
78-
schema_editor.execute(statement)
98+
create_model_indexes(MockItem)
7999

80100
return MockItem.objects.count()
81101

paradedb/indexes.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,18 @@ class IndexExpression:
165165
tokenizer: Tokenizer | None = None
166166

167167

168+
DEFAULT_INDEX_AM = "paradedb"
169+
SUPPORTED_INDEX_AMS = ("paradedb", "bm25")
170+
171+
168172
class BM25Index(models.Index):
169-
"""BM25 index for ParadeDB."""
173+
"""BM25 index for ParadeDB.
174+
175+
By default the index is created with ``USING paradedb``, the primary
176+
access method name in pg_search 0.25.0+. Pass ``am="bm25"`` to emit
177+
``USING bm25``, the permanent backwards-compatible alias supported by
178+
all pg_search versions.
179+
"""
170180

171181
suffix = "bm25"
172182

@@ -178,10 +188,15 @@ def __init__(
178188
name: str,
179189
expressions: list[IndexExpression] | None = None,
180190
condition: models.Q | None = None,
191+
am: str = DEFAULT_INDEX_AM,
181192
) -> None:
193+
if am not in SUPPORTED_INDEX_AMS:
194+
supported = ", ".join(repr(name) for name in SUPPORTED_INDEX_AMS)
195+
raise ValueError(f"Index access method must be one of: {supported}.")
182196
self.fields_config = fields
183197
self.key_field = key_field
184198
self.index_expressions = list(expressions or [])
199+
self.am = am
185200
super().__init__(name=name, fields=list(fields.keys()), condition=condition)
186201

187202
def deconstruct(self) -> tuple[str, Any, dict[str, Any]]:
@@ -191,6 +206,8 @@ def deconstruct(self) -> tuple[str, Any, dict[str, Any]]:
191206
kwargs["name"] = self.name
192207
if self.index_expressions:
193208
kwargs["expressions"] = self.index_expressions
209+
if self.am != DEFAULT_INDEX_AM:
210+
kwargs["am"] = self.am
194211
return path, args, kwargs
195212

196213
def create_sql(
@@ -218,7 +235,7 @@ def create_sql(
218235
create_stmt += " CONCURRENTLY"
219236
template = (
220237
f"{create_stmt} %(name)s ON %(table)s\n"
221-
"USING bm25 (\n"
238+
f"USING {self.am} (\n"
222239
" %(expressions)s\n"
223240
")\n"
224241
f"WITH ({', '.join(storage_params)})"

tests/conftest.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ def pytest_configure(config: object) -> None:
3939
raise RuntimeError("psycopg is required to run integration tests") from exc
4040

4141

42+
_server_index_am: str | None = None
43+
44+
45+
def server_index_am() -> str:
46+
"""AM used by the integration suite: `paradedb` when the server has it
47+
(pg_search 0.25.0+), otherwise the `bm25` alias."""
48+
global _server_index_am
49+
if _server_index_am is None:
50+
with connection.cursor() as cursor:
51+
cursor.execute("SELECT 1 FROM pg_am WHERE amname = 'paradedb';")
52+
_server_index_am = "paradedb" if cursor.fetchone() else "bm25"
53+
return _server_index_am
54+
55+
4256
def _require_postgres() -> None:
4357
engine = connection.settings_dict.get("ENGINE", "")
4458
if "postgresql" not in engine:
@@ -66,7 +80,7 @@ def paradedb_ready(django_db_setup: object, django_db_blocker: object) -> None:
6680

6781
with django_db_blocker.unblock(), connection.cursor() as cursor:
6882
try:
69-
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search;")
83+
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_search CASCADE;")
7084
except Exception as exc: # pragma: no cover - defensive skip
7185
pytest.fail(
7286
f"ParadeDB pg_search extension unavailable in target database: {exc}"
@@ -76,7 +90,7 @@ def paradedb_ready(django_db_setup: object, django_db_blocker: object) -> None:
7690
)
7791
cursor.execute("DROP INDEX IF EXISTS mock_items_bm25_idx;")
7892
cursor.execute(
79-
"CREATE INDEX mock_items_bm25_idx ON mock_items USING bm25 ("
93+
f"CREATE INDEX mock_items_bm25_idx ON mock_items USING {server_index_am()} ("
8094
"id, "
8195
"description, "
8296
"category, "

0 commit comments

Comments
 (0)