Skip to content
Merged
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
48 changes: 43 additions & 5 deletions django_mongodb_backend/indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,38 @@ class SearchIndex(Index):
suffix = "six"
_error_id_prefix = "django_mongodb_backend.indexes.SearchIndex"

def __init__(self, *, fields=(), name=None):
def __init__(
self, *, fields=(), field_mappings=None, name=None, analyzer=None, search_analyzer=None
Copy link
Collaborator

Choose a reason for hiding this comment

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

While it's called "field mappings" in the MongoDB docs, I've been struggling to intuitively remember where the "s" goes (field_mappings, fields_mapping, fields_mappings, etc.). I think fields_mappings may be more intuitive since we have an existing fields parameter and also the JSON structure has ["mappings"] and ["fields"]. What do you think?

Copy link
Contributor Author

@Jibola Jibola Oct 9, 2025

Choose a reason for hiding this comment

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

Hrm, after thinking it over, I say we still keep it field_mappings. I understand the dangling "s" concern. Since at the server level, fields and field_mappings are the actual names of the keys, keeping that parallelism still aligns better to me.
cc:
@aclark4life , @WaVEV for additional opinions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

To clarify, the documentation uses the term "Field Mappings" but I haven't seen a key called "field_mappings". Did I miss it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

🤔 IMO, field_mappings is more accurate. Noun + noun (compound noun) to refer to a new noun the first one is in singular. Like copilot settings.

Copy link
Contributor Author

@Jibola Jibola Oct 13, 2025

Choose a reason for hiding this comment

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

@timgraham my mistake. I vaguely remember seeing field_mappings, in my earlier attempts at this, but I'm guessing it's that same documentation that I saw and attributed. Nonetheless, I still align with the field_mappings because it's also just phonetically better. (to @WaVEV's point as well)

):
if field_mappings and not isinstance(field_mappings, dict):
raise ValueError(
"field_mappings must be a dictionary mapping field names to their "
"Atlas Search index options."
)
if analyzer and not isinstance(analyzer, str):
raise ValueError(f"analyzer must be a string; got: {type(analyzer)}.")
if search_analyzer and not isinstance(search_analyzer, str):
raise ValueError(f"search_analyzer must be a string; got: {type(search_analyzer)}.")
self.field_mappings = field_mappings
self.analyzer = analyzer
self.search_analyzer = search_analyzer
if field_mappings:
if fields:
raise ValueError("Cannot provide fields and field_mappings.")
fields = [*self.field_mappings.keys()]
super().__init__(fields=fields, name=name)

def deconstruct(self):
path, args, kwargs = super().deconstruct()
if self.field_mappings:
kwargs["field_mappings"] = self.field_mappings
del kwargs["fields"]
if self.analyzer:
kwargs["analyzer"] = self.analyzer
if self.search_analyzer:
kwargs["search_analyzer"] = self.search_analyzer
return path, args, kwargs

def check(self, model, connection):
errors = []
if not connection.features.supports_atlas_search:
Expand Down Expand Up @@ -152,12 +181,21 @@ def get_pymongo_index_model(
return None
fields = {}
for field_name, _ in self.fields_orders:
field = model._meta.get_field(field_name)
type_ = self.search_index_data_types(field.db_type(schema_editor.connection))
field_path = column_prefix + model._meta.get_field(field_name).column
fields[field_path] = {"type": type_}
if self.field_mappings:
fields[field_path] = self.field_mappings[field_name]
else:
field = model._meta.get_field(field_name)
type_ = self.search_index_data_types(field.db_type(schema_editor.connection))
fields[field_path] = {"type": type_}
extra = {}
if self.analyzer:
extra["analyzer"] = self.analyzer
if self.search_analyzer:
extra["searchAnalyzer"] = self.search_analyzer
return SearchIndexModel(
definition={"mappings": {"dynamic": False, "fields": fields}}, name=self.name
definition={"mappings": {"dynamic": False, "fields": fields}, **extra},
name=self.name,
)


Expand Down
8 changes: 6 additions & 2 deletions django_mongodb_backend/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ def _get_search_index_info(self, table_name):
type_ = VectorSearchIndex.suffix
options = details
else:
options = details["latestDefinition"]["mappings"]
columns = list(options.get("fields", {}).keys())
options = {
"analyzer": details["latestDefinition"].get("analyzer"),
"searchAnalyzer": details["latestDefinition"].get("searchAnalyzer"),
"mappings": details["latestDefinition"]["mappings"],
}
columns = list(options["mappings"].get("fields", {}).keys())
type_ = SearchIndex.suffix
constraints[details["name"]] = {
"check": False,
Expand Down
18 changes: 17 additions & 1 deletion docs/ref/models/indexes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ minutes, depending on the size of the collection.
``SearchIndex``
---------------

.. class:: SearchIndex(fields=(), name=None)
.. class:: SearchIndex(fields=(), field_mappings=None, name=None, analyzer=None, search_analyzer=None)

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

Use ``field_mappings`` (instead of ``fields``) to create a complex search
index. ``field_mappings`` is a dictionary that maps field names to index
options. It corresponds to ``definition["mappings"]["fields"]`` in the
:ref:`atlas:fts-static-mapping-examples`.

If ``name`` isn't provided, one will be generated automatically. If you
need to reference the name in your search query and don't provide your own
name, you can lookup the generated one using ``Model._meta.indexes[0].name``
(substituting the name of your model as well as a different list index if
your model has multiple indexes).

Use ``analyzer`` and ``search_analyzer`` to configure the indexing and
searching :doc:`analyzer <atlas:atlas-search/analyzers>`. If these options
aren't provided, the server defaults to ``lucene.standard``. It corresponds
to ``definition["analyzer"]`` and ``definition["searchAnalyzer"]`` in the
:ref:`atlas:fts-static-mapping-examples`.

.. versionchanged:: 5.2.2

The ``field_mappings``, ``analyzer``, and ``search_analyzer`` arguments
were added.

``VectorSearchIndex``
---------------------

Expand Down
3 changes: 2 additions & 1 deletion docs/releases/5.2.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Django MongoDB Backend 5.2.x
New features
------------

- ...
- :class:`.SearchIndex`\'s new ``field_mappings``, ``analyzer``, and
``search_analyzer`` arguments allow creating more complex indexes.

Bug fixes
---------
Expand Down
Loading