Skip to content

Commit f9be438

Browse files
committed
INTPYTHON-730 Make search index creation and deletion synchronous
1 parent d7f8fd2 commit f9be438

File tree

6 files changed

+58
-23
lines changed

6 files changed

+58
-23
lines changed

django_mongodb_backend/schema.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from time import monotonic, sleep
2+
13
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
24
from django.db.models import Index, UniqueConstraint
35
from pymongo.operations import SearchIndexModel
@@ -269,10 +271,12 @@ def add_index(
269271
)
270272
if idx:
271273
model = parent_model or model
274+
collection = self.get_collection(model._meta.db_table)
272275
if isinstance(idx, SearchIndexModel):
273-
self.get_collection(model._meta.db_table).create_search_index(idx)
276+
collection.create_search_index(idx)
277+
self.wait_until_index_created(collection, index.name)
274278
else:
275-
self.get_collection(model._meta.db_table).create_indexes([idx])
279+
collection.create_indexes([idx])
276280

277281
def _add_composed_index(self, model, field_names, column_prefix="", parent_model=None):
278282
"""Add an index on the given list of field_names."""
@@ -290,12 +294,14 @@ def _add_field_index(self, model, field, *, column_prefix=""):
290294
def remove_index(self, model, index):
291295
if index.contains_expressions:
292296
return
297+
collection = self.get_collection(model._meta.db_table)
293298
if isinstance(index, SearchIndex):
294299
# Drop the index if it's supported.
295300
if self.connection.features.supports_atlas_search:
296-
self.get_collection(model._meta.db_table).drop_search_index(index.name)
301+
collection.drop_search_index(index.name)
302+
self.wait_until_index_deleted(collection, index.name)
297303
else:
298-
self.get_collection(model._meta.db_table).drop_index(index.name)
304+
collection.drop_index(index.name)
299305

300306
def _remove_composed_index(
301307
self, model, field_names, constraint_kwargs, column_prefix="", parent_model=None
@@ -420,6 +426,27 @@ def _field_should_have_unique(self, field):
420426
# The _id column is automatically unique.
421427
return db_type and field.unique and field.column != "_id"
422428

429+
@staticmethod
430+
def wait_until_index_created(collection, index_name, timeout=60 * 60, interval=0.5):
431+
start = monotonic()
432+
while monotonic() - start < timeout:
433+
indexes = list(collection.list_search_indexes())
434+
for idx in indexes:
435+
if idx["name"] == index_name and idx["status"] == "READY":
436+
return True
437+
sleep(interval)
438+
raise TimeoutError(f"Index {index_name} not ready after {timeout} seconds.")
439+
440+
@staticmethod
441+
def wait_until_index_deleted(collection, index_name, timeout=60, interval=0.5):
442+
start = monotonic()
443+
while monotonic() - start < timeout:
444+
indexes = list(collection.list_search_indexes())
445+
if all(idx["name"] != index_name for idx in indexes):
446+
return True
447+
sleep(interval)
448+
raise TimeoutError(f"Index {index_name} not deleted after {timeout} seconds.")
449+
423450

424451
# GISSchemaEditor extends some SchemaEditor methods.
425452
class DatabaseSchemaEditor(GISSchemaEditor, BaseSchemaEditor):

docs/ref/models/indexes.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@ Some MongoDB-specific :doc:`indexes <django:ref/models/indexes>`, for use on a
99
model's :attr:`Meta.indexes <django.db.models.Options.indexes>` option, are
1010
available in ``django_mongodb_backend.indexes``.
1111

12+
Search indexes
13+
==============
14+
15+
MongoDB creates these indexes asynchronously, however, Django's
16+
:class:`~django.db.migrations.operations.AddIndex` and
17+
:class:`~django.db.migrations.operations.RemoveIndex` operations will wait
18+
until the index is created or deleted so that the database state is
19+
consistent in the operations that follow. Adding indexes may take seconds or
20+
minutes, depending on the size of the collection.
21+
22+
.. versionchanged:: 5.2.1
23+
24+
The aforementioned waiting was added.
25+
1226
``SearchIndex``
13-
===============
27+
---------------
1428

1529
.. class:: SearchIndex(fields=(), name=None)
1630

@@ -30,7 +44,7 @@ available in ``django_mongodb_backend.indexes``.
3044
your model has multiple indexes).
3145

3246
``VectorSearchIndex``
33-
=====================
47+
---------------------
3448

3549
.. class:: VectorSearchIndex(*, fields=(), name=None, similarities)
3650

docs/releases/5.2.x.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ New features
1515
Bug fixes
1616
---------
1717

18-
- ...
18+
- Migrations operations that add or delete search indexes now wait until the
19+
operation is completed on the server to prevent conflicts when running
20+
multiple operations sequentially.
1921

2022
5.2.0
2123
=====

tests/atlas_search_/test_search.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,11 @@
2828
SearchVector,
2929
SearchWildcard,
3030
)
31+
from django_mongodb_backend.schema import DatabaseSchemaEditor
3132

3233
from .models import Article, Location, Writer
3334

3435

35-
def wait_until_index_ready(collection, index_name, timeout: float = 5, interval: float = 0.5):
36-
start = monotonic()
37-
while monotonic() - start < timeout:
38-
indexes = list(collection.list_search_indexes())
39-
for idx in indexes:
40-
if idx["name"] == index_name and idx["status"] == "READY":
41-
return True
42-
sleep(interval)
43-
raise TimeoutError(f"Index {index_name} not ready after {timeout} seconds")
44-
45-
4636
def _delayed_assertion(timeout: float = 4, interval: float = 0.5):
4737
def decorator(assert_func):
4838
@wraps(assert_func)
@@ -91,13 +81,16 @@ def _get_collection(model):
9181

9282
@classmethod
9383
def create_search_index(cls, model, index_name, definition, type="search"):
84+
# TODO: create/delete indexes using DatabaseSchemaEditor when
85+
# SearchIndexes support mappings (INTPYTHON-729).
9486
collection = cls._get_collection(model)
9587
idx = SearchIndexModel(definition=definition, name=index_name, type=type)
9688
collection.create_search_index(idx)
97-
wait_until_index_ready(collection, index_name)
89+
DatabaseSchemaEditor.wait_until_index_created(collection, index_name)
9890

9991
def drop_index():
10092
collection.drop_search_index(index_name)
93+
DatabaseSchemaEditor.wait_until_index_deleted(collection, index_name)
10194

10295
cls.addClassCleanup(drop_index)
10396

tests/indexes_/test_base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
class SchemaAssertionMixin:
55
def assertAddRemoveIndex(self, editor, model, index):
6-
with self.assertNumQueries(1):
7-
editor.add_index(index=index, model=model)
6+
editor.add_index(index=index, model=model)
87
try:
98
self.assertIn(
109
index.name,

tests/indexes_/test_search_indexes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def test_multiple_fields(self):
237237
},
238238
"latestVersion": 0,
239239
"name": "recent_test_idx",
240-
"queryable": False,
240+
"queryable": True,
241241
"type": "vectorSearch",
242242
}
243243
self.assertCountEqual(index_info[index.name]["columns"], index.fields)
@@ -280,7 +280,7 @@ def test_similarities_list(self):
280280
},
281281
"latestVersion": 0,
282282
"name": "recent_test_idx",
283-
"queryable": False,
283+
"queryable": True,
284284
"type": "vectorSearch",
285285
}
286286
self.assertCountEqual(index_info[index.name]["columns"], index.fields)

0 commit comments

Comments
 (0)