Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions django_mongodb_backend/schema.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from time import monotonic, sleep

from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.models import Index, UniqueConstraint
from pymongo.operations import SearchIndexModel
Expand Down Expand Up @@ -269,10 +271,12 @@ def add_index(
)
if idx:
model = parent_model or model
collection = self.get_collection(model._meta.db_table)
if isinstance(idx, SearchIndexModel):
self.get_collection(model._meta.db_table).create_search_index(idx)
collection.create_search_index(idx)
self.wait_until_index_created(collection, index.name)
else:
self.get_collection(model._meta.db_table).create_indexes([idx])
collection.create_indexes([idx])

def _add_composed_index(self, model, field_names, column_prefix="", parent_model=None):
"""Add an index on the given list of field_names."""
Expand All @@ -290,12 +294,14 @@ def _add_field_index(self, model, field, *, column_prefix=""):
def remove_index(self, model, index):
if index.contains_expressions:
return
collection = self.get_collection(model._meta.db_table)
if isinstance(index, SearchIndex):
# Drop the index if it's supported.
if self.connection.features.supports_atlas_search:
self.get_collection(model._meta.db_table).drop_search_index(index.name)
collection.drop_search_index(index.name)
self.wait_until_index_deleted(collection, index.name)
else:
self.get_collection(model._meta.db_table).drop_index(index.name)
collection.drop_index(index.name)

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

@staticmethod
def wait_until_index_created(collection, index_name, timeout=60 * 60, interval=0.5):
start = monotonic()
while monotonic() - start < timeout:
indexes = list(collection.list_search_indexes())
for idx in indexes:
if idx["name"] == index_name and idx["status"] == "READY":
return True
sleep(interval)
raise TimeoutError(f"Index {index_name} not ready after {timeout} seconds.")

@staticmethod
def wait_until_index_deleted(collection, index_name, timeout=60, interval=0.5):
start = monotonic()
while monotonic() - start < timeout:
indexes = list(collection.list_search_indexes())
if all(idx["name"] != index_name for idx in indexes):
return True
sleep(interval)
raise TimeoutError(f"Index {index_name} not deleted after {timeout} seconds.")


# GISSchemaEditor extends some SchemaEditor methods.
class DatabaseSchemaEditor(GISSchemaEditor, BaseSchemaEditor):
Expand Down
18 changes: 16 additions & 2 deletions docs/ref/models/indexes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,22 @@ Some MongoDB-specific :doc:`indexes <django:ref/models/indexes>`, for use on a
model's :attr:`Meta.indexes <django.db.models.Options.indexes>` option, are
available in ``django_mongodb_backend.indexes``.

Search indexes
==============

MongoDB creates these indexes asynchronously, however, Django's
:class:`~django.db.migrations.operations.AddIndex` and
:class:`~django.db.migrations.operations.RemoveIndex` operations will wait
until the index is created or deleted so that the database state is
consistent in the operations that follow. Adding indexes may take seconds or
minutes, depending on the size of the collection.

.. versionchanged:: 5.2.1

The aforementioned waiting was added.

``SearchIndex``
===============
---------------

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

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

``VectorSearchIndex``
=====================
---------------------

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

Expand Down
4 changes: 3 additions & 1 deletion docs/releases/5.2.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ New features
Bug fixes
---------

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

5.2.0
=====
Expand Down
17 changes: 5 additions & 12 deletions tests/atlas_search_/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,11 @@
SearchVector,
SearchWildcard,
)
from django_mongodb_backend.schema import DatabaseSchemaEditor

from .models import Article, Location, Writer


def wait_until_index_ready(collection, index_name, timeout: float = 5, interval: float = 0.5):
start = monotonic()
while monotonic() - start < timeout:
indexes = list(collection.list_search_indexes())
for idx in indexes:
if idx["name"] == index_name and idx["status"] == "READY":
return True
sleep(interval)
raise TimeoutError(f"Index {index_name} not ready after {timeout} seconds")


def _delayed_assertion(timeout: float = 4, interval: float = 0.5):
def decorator(assert_func):
@wraps(assert_func)
Expand Down Expand Up @@ -91,13 +81,16 @@ def _get_collection(model):

@classmethod
def create_search_index(cls, model, index_name, definition, type="search"):
# TODO: create/delete indexes using DatabaseSchemaEditor when
# SearchIndexes support mappings (INTPYTHON-729).
collection = cls._get_collection(model)
idx = SearchIndexModel(definition=definition, name=index_name, type=type)
collection.create_search_index(idx)
wait_until_index_ready(collection, index_name)
DatabaseSchemaEditor.wait_until_index_created(collection, index_name)

def drop_index():
collection.drop_search_index(index_name)
DatabaseSchemaEditor.wait_until_index_deleted(collection, index_name)

cls.addClassCleanup(drop_index)

Expand Down
3 changes: 1 addition & 2 deletions tests/indexes_/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

class SchemaAssertionMixin:
def assertAddRemoveIndex(self, editor, model, index):
with self.assertNumQueries(1):
editor.add_index(index=index, model=model)
editor.add_index(index=index, model=model)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be some number of list_search_indexes() queries until the index is ready.

try:
self.assertIn(
index.name,
Expand Down
4 changes: 2 additions & 2 deletions tests/indexes_/test_search_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def test_multiple_fields(self):
},
"latestVersion": 0,
"name": "recent_test_idx",
"queryable": False,
"queryable": True,
"type": "vectorSearch",
}
self.assertCountEqual(index_info[index.name]["columns"], index.fields)
Expand Down Expand Up @@ -280,7 +280,7 @@ def test_similarities_list(self):
},
"latestVersion": 0,
"name": "recent_test_idx",
"queryable": False,
"queryable": True,
"type": "vectorSearch",
}
self.assertCountEqual(index_info[index.name]["columns"], index.fields)
Expand Down