Skip to content

Commit db4b32f

Browse files
Jibolatimgraham
authored andcommitted
INTPYTHON-729 Allow creating search indexes with field mappings
1 parent e747973 commit db4b32f

File tree

5 files changed

+109
-109
lines changed

5 files changed

+109
-109
lines changed

django_mongodb_backend/indexes.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,25 @@ class SearchIndex(Index):
109109
suffix = "six"
110110
_error_id_prefix = "django_mongodb_backend.indexes.SearchIndex"
111111

112-
def __init__(self, *, fields=(), name=None):
112+
def __init__(self, *, fields=(), field_mappings=None, name=None):
113+
if field_mappings and not isinstance(field_mappings, dict):
114+
raise ValueError(
115+
"field_mappings must be a dictionary mapping field names to their "
116+
"Atlas Search field mappings."
117+
)
118+
self.field_mappings = field_mappings
119+
if field_mappings:
120+
if fields:
121+
raise ValueError("Cannot provide fields and fields_mappings")
122+
fields = [*self.field_mappings.keys()]
113123
super().__init__(fields=fields, name=name)
114124

125+
def deconstruct(self):
126+
path, args, kwargs = super().deconstruct()
127+
if self.field_mappings is not None:
128+
kwargs["field_mappings"] = self.field_mappings
129+
return path, args, kwargs
130+
115131
def check(self, model, connection):
116132
errors = []
117133
if not connection.features.supports_atlas_search:
@@ -152,15 +168,30 @@ def get_pymongo_index_model(
152168
return None
153169
fields = {}
154170
for field_name, _ in self.fields_orders:
155-
field = model._meta.get_field(field_name)
156-
type_ = self.search_index_data_types(field.db_type(schema_editor.connection))
157171
field_path = column_prefix + model._meta.get_field(field_name).column
158-
fields[field_path] = {"type": type_}
172+
if self.field_mappings:
173+
fields[field_path] = self.field_mappings[field_name]
174+
else:
175+
field = model._meta.get_field(field_name)
176+
type_ = self.search_index_data_types(field.db_type(schema_editor.connection))
177+
fields[field_path] = {"type": type_}
159178
return SearchIndexModel(
160179
definition={"mappings": {"dynamic": False, "fields": fields}}, name=self.name
161180
)
162181

163182

183+
class DynamicSearchIndex(SearchIndex):
184+
suffix = "dsix"
185+
_error_id_prefix = "django_mongodb_backend.indexes.DynamicSearchIndex"
186+
187+
def get_pymongo_index_model(
188+
self, model, schema_editor, field=None, unique=False, column_prefix=""
189+
):
190+
if not schema_editor.connection.features.supports_atlas_search:
191+
return None
192+
return SearchIndexModel(definition={"mappings": {"dynamic": True}}, name=self.name)
193+
194+
164195
class VectorSearchIndex(SearchIndex):
165196
suffix = "vsi"
166197
_error_id_prefix = "django_mongodb_backend.indexes.VectorSearchIndex"

docs/ref/models/indexes.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ minutes, depending on the size of the collection.
2626
``SearchIndex``
2727
---------------
2828

29-
.. class:: SearchIndex(fields=(), name=None)
29+
.. class:: SearchIndex(fields=(), field_mappings=None, name=None)
3030

3131
Creates a basic :doc:`search index <atlas:atlas-search/index-definitions>`
3232
on the given field(s).
@@ -35,12 +35,21 @@ minutes, depending on the size of the collection.
3535
supported. See the :ref:`Atlas documentation <atlas:bson-data-chart>` for a
3636
complete list of unsupported data types.
3737

38+
Use ``field_mappings`` (instead of ``fields``) to create an advanced search
39+
index. ``field_mappings`` is a dictionary that maps field names to index
40+
options (see ``definition["mappings"]["fields"]`` in the
41+
:ref:`atlas:fts-static-mapping-example`).
42+
3843
If ``name`` isn't provided, one will be generated automatically. If you
3944
need to reference the name in your search query and don't provide your own
4045
name, you can lookup the generated one using ``Model._meta.indexes[0].name``
4146
(substituting the name of your model as well as a different list index if
4247
your model has multiple indexes).
4348

49+
.. versionchanged:: 5.2.2
50+
51+
The ``fields_mappings`` argument was added.
52+
4453
``VectorSearchIndex``
4554
---------------------
4655

docs/releases/5.2.x.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Django MongoDB Backend 5.2.x
1010
New features
1111
------------
1212

13-
- ...
13+
- Added the ``field_mappings`` argument to :class:`.SearchIndex` to allow
14+
creating advanced indexes.
1415

1516
Bug fixes
1617
---------

tests/atlas_search_/test_search.py

Lines changed: 49 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from django.db.models.query import QuerySet
99
from django.db.utils import DatabaseError
1010
from django.test import TransactionTestCase, skipUnlessDBFeature
11-
from pymongo.operations import SearchIndexModel
1211

1312
from django_mongodb_backend.expressions import (
1413
CompoundExpression,
@@ -28,7 +27,7 @@
2827
SearchVector,
2928
SearchWildcard,
3029
)
31-
from django_mongodb_backend.schema import DatabaseSchemaEditor
30+
from django_mongodb_backend.indexes import SearchIndex, VectorSearchIndex
3231

3332
from .models import Article, Location, Writer
3433

@@ -75,22 +74,15 @@ class SearchUtilsMixin(TransactionTestCase):
7574
assertListEqual = _delayed_assertion(timeout=2)(TransactionTestCase.assertListEqual)
7675
assertQuerySetEqual = _delayed_assertion(timeout=2)(TransactionTestCase.assertQuerySetEqual)
7776

78-
@staticmethod
79-
def _get_collection(model):
80-
return connection.database.get_collection(model._meta.db_table)
81-
8277
@classmethod
83-
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).
86-
collection = cls._get_collection(model)
87-
idx = SearchIndexModel(definition=definition, name=index_name, type=type)
88-
collection.create_search_index(idx)
89-
DatabaseSchemaEditor.wait_until_index_created(collection, index_name)
78+
def create_search_index(cls, model, index_name, definition, index_cls=SearchIndex):
79+
idx = index_cls(field_mappings=definition, name=index_name)
80+
with connection.schema_editor() as editor:
81+
editor.add_index(model, idx)
9082

9183
def drop_index():
92-
collection.drop_search_index(index_name)
93-
DatabaseSchemaEditor.wait_until_index_dropped(collection, index_name)
84+
with connection.schema_editor() as editor:
85+
editor.remove_index(model, idx)
9486

9587
cls.addClassCleanup(drop_index)
9688

@@ -101,12 +93,7 @@ def setUpClass(cls):
10193
cls.create_search_index(
10294
Article,
10395
"equals_headline_index",
104-
{
105-
"mappings": {
106-
"dynamic": False,
107-
"fields": {"headline": {"type": "token"}, "number": {"type": "number"}},
108-
}
109-
},
96+
{"headline": {"type": "token"}, "number": {"type": "number"}},
11097
)
11198

11299
def setUp(self):
@@ -167,32 +154,27 @@ def setUpClass(cls):
167154
Article,
168155
"autocomplete_headline_index",
169156
{
170-
"mappings": {
171-
"dynamic": False,
157+
"headline": {
158+
"type": "autocomplete",
159+
"analyzer": "lucene.standard",
160+
"tokenization": "edgeGram",
161+
"minGrams": 3,
162+
"maxGrams": 5,
163+
"foldDiacritics": False,
164+
},
165+
"writer": {
166+
"type": "document",
172167
"fields": {
173-
"headline": {
168+
"name": {
174169
"type": "autocomplete",
175170
"analyzer": "lucene.standard",
176171
"tokenization": "edgeGram",
177172
"minGrams": 3,
178173
"maxGrams": 5,
179174
"foldDiacritics": False,
180-
},
181-
"writer": {
182-
"type": "document",
183-
"fields": {
184-
"name": {
185-
"type": "autocomplete",
186-
"analyzer": "lucene.standard",
187-
"tokenization": "edgeGram",
188-
"minGrams": 3,
189-
"maxGrams": 5,
190-
"foldDiacritics": False,
191-
}
192-
},
193-
},
175+
}
194176
},
195-
}
177+
},
196178
},
197179
)
198180

@@ -253,7 +235,7 @@ def setUpClass(cls):
253235
cls.create_search_index(
254236
Article,
255237
"exists_body_index",
256-
{"mappings": {"dynamic": False, "fields": {"body": {"type": "token"}}}},
238+
{"body": {"type": "token"}},
257239
)
258240

259241
def setUp(self):
@@ -282,7 +264,7 @@ def setUpClass(cls):
282264
cls.create_search_index(
283265
Article,
284266
"in_headline_index",
285-
{"mappings": {"dynamic": False, "fields": {"headline": {"type": "token"}}}},
267+
{"headline": {"type": "token"}},
286268
)
287269

288270
def setUp(self):
@@ -316,7 +298,7 @@ def setUpClass(cls):
316298
cls.create_search_index(
317299
Article,
318300
"phrase_body_index",
319-
{"mappings": {"dynamic": False, "fields": {"body": {"type": "string"}}}},
301+
{"body": {"type": "string"}},
320302
)
321303

322304
def setUp(self):
@@ -356,13 +338,8 @@ def setUpClass(cls):
356338
Article,
357339
"query_string_index",
358340
{
359-
"mappings": {
360-
"dynamic": False,
361-
"fields": {
362-
"headline": {"type": "string"},
363-
"body": {"type": "string"},
364-
},
365-
}
341+
"headline": {"type": "string"},
342+
"body": {"type": "string"},
366343
},
367344
)
368345

@@ -416,7 +393,7 @@ def setUpClass(cls):
416393
cls.create_search_index(
417394
Article,
418395
"range_number_index",
419-
{"mappings": {"dynamic": False, "fields": {"number": {"type": "number"}}}},
396+
{"number": {"type": "number"}},
420397
)
421398
Article.objects.create(headline="x", number=5, body="z")
422399

@@ -453,12 +430,7 @@ def setUpClass(cls):
453430
cls.create_search_index(
454431
Article,
455432
"regex_headline_index",
456-
{
457-
"mappings": {
458-
"dynamic": False,
459-
"fields": {"headline": {"type": "string", "analyzer": "lucene.keyword"}},
460-
}
461-
},
433+
{"headline": {"type": "string", "analyzer": "lucene.keyword"}},
462434
)
463435

464436
def setUp(self):
@@ -498,7 +470,7 @@ def setUpClass(cls):
498470
cls.create_search_index(
499471
Article,
500472
"text_body_index",
501-
{"mappings": {"dynamic": False, "fields": {"body": {"type": "string"}}}},
473+
{"body": {"type": "string"}},
502474
)
503475

504476
def setUp(self):
@@ -560,12 +532,7 @@ def setUpClass(cls):
560532
cls.create_search_index(
561533
Article,
562534
"wildcard_headline_index",
563-
{
564-
"mappings": {
565-
"dynamic": False,
566-
"fields": {"headline": {"type": "string", "analyzer": "lucene.keyword"}},
567-
}
568-
},
535+
{"headline": {"type": "string", "analyzer": "lucene.keyword"}},
569536
)
570537

571538
def setUp(self):
@@ -603,12 +570,7 @@ def setUpClass(cls):
603570
cls.create_search_index(
604571
Article,
605572
"geoshape_location_index",
606-
{
607-
"mappings": {
608-
"dynamic": False,
609-
"fields": {"location": {"type": "geo", "indexShapes": True}},
610-
}
611-
},
573+
{"location": {"type": "geo", "indexShapes": True}},
612574
)
613575

614576
def setUp(self):
@@ -668,7 +630,7 @@ def setUpClass(cls):
668630
cls.create_search_index(
669631
Article,
670632
"geowithin_location_index",
671-
{"mappings": {"dynamic": False, "fields": {"location": {"type": "geo"}}}},
633+
{"location": {"type": "geo"}},
672634
)
673635

674636
def setUp(self):
@@ -743,12 +705,7 @@ def setUpClass(cls):
743705
cls.create_search_index(
744706
Article,
745707
"mlt_index",
746-
{
747-
"mappings": {
748-
"dynamic": False,
749-
"fields": {"body": {"type": "string"}, "headline": {"type": "string"}},
750-
}
751-
},
708+
{"body": {"type": "string"}, "headline": {"type": "string"}},
752709
)
753710
cls.article1 = Article.objects.create(
754711
headline="Space exploration", number=1, body="Webb telescope"
@@ -782,14 +739,9 @@ def setUpClass(cls):
782739
Article,
783740
"compound_index",
784741
{
785-
"mappings": {
786-
"dynamic": False,
787-
"fields": {
788-
"headline": [{"type": "token"}, {"type": "string"}],
789-
"body": {"type": "string"},
790-
"number": {"type": "number"},
791-
},
792-
}
742+
"headline": [{"type": "token"}, {"type": "string"}],
743+
"body": {"type": "string"},
744+
"number": {"type": "number"},
793745
},
794746
)
795747

@@ -962,26 +914,20 @@ def test_str_returns_expected_format(self):
962914
class SearchVectorTests(SearchUtilsMixin):
963915
@classmethod
964916
def setUpClass(cls):
965-
cls.create_search_index(
966-
Article,
967-
"vector_index",
968-
{
969-
"fields": [
970-
{
971-
"type": "vector",
972-
"path": "plot_embedding",
973-
"numDimensions": 3,
974-
"similarity": "cosine",
975-
"quantization": "scalar",
976-
},
977-
{
978-
"type": "filter",
979-
"path": "number",
980-
},
981-
]
982-
},
983-
type="vectorSearch",
917+
model = Article
918+
idx = VectorSearchIndex(
919+
fields=["plot_embedding", "number"],
920+
name="vector_index",
921+
similarities="cosine",
984922
)
923+
with connection.schema_editor() as editor:
924+
editor.add_index(model, idx)
925+
926+
def drop_index():
927+
with connection.schema_editor() as editor:
928+
editor.remove_index(model, idx)
929+
930+
cls.addClassCleanup(drop_index)
985931

986932
def setUp(self):
987933
self.mars = Article.objects.create(

0 commit comments

Comments
 (0)