diff --git a/docs/changelog.rst b/docs/changelog.rst index fbba81685..e73c94b49 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ Changes in 1.0.0 - Bug Fix - Fix Document.compare_indexes() not working correctly for text indexes on multiple fields #2612 - BREAKING CHANGE: wrap _document_registry (normally not used by end users) with _DocumentRegistry which acts as a singleton to access the registry - Log a warning in case users creates multiple Document classes with the same name as it can lead to unexpected behavior #1778 +- Fix use of $search, $searchMeta, or $vectorSearch in aggregate #2878 - BugFix - Fix use of $geoNear or $collStats in aggregate #2493 - BREAKING CHANGE: Further to the deprecation warning, remove ability to use an unpacked list to `Queryset.aggregate(*pipeline)`, a plain list must be provided instead `Queryset.aggregate(pipeline)`, as it's closer to pymongo interface - BREAKING CHANGE: Further to the deprecation warning, remove `full_response` from `QuerySet.modify` as it wasn't supported with Pymongo 3+ diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c17dc3303..5556181a4 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1382,13 +1382,20 @@ def aggregate(self, pipeline, **kwargs): if self._skip is not None: initial_pipeline.append({"$skip": self._skip}) - # geoNear and collStats must be the first stages in the pipeline if present + # Some aggregation stages must precede MongoEngine's implicit stages. first_step = [] new_user_pipeline = [] for step_step in pipeline: - if "$geoNear" in step_step: - first_step.append(step_step) - elif "$collStats" in step_step: + if any( + operator in step_step + for operator in ( + "$geoNear", + "$collStats", + "$search", + "$searchMeta", + "$vectorSearch", + ) + ): first_step.append(step_step) else: new_user_pipeline.append(step_step) diff --git a/tests/queryset/test_queryset_aggregation.py b/tests/queryset/test_queryset_aggregation.py index 7e390e35a..f2fc0139c 100644 --- a/tests/queryset/test_queryset_aggregation.py +++ b/tests/queryset/test_queryset_aggregation.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from pymongo.read_preferences import ReadPreference @@ -373,3 +375,21 @@ class SomeDoc(Document): res = list(SomeDoc.objects.aggregate(pipeline)) assert len(res) == 1 assert res[0]["count"] == 2 + + def test_aggregate__search_stage__precedes_implicit_match(self): + class SearchableDoc(Document): + last_name = StringField() + + for search_stage in ("$search", "$searchMeta", "$vectorSearch"): + with self.subTest(search_stage=search_stage): + search_step = {search_stage: {}} + + # End-to-end execution requires MongoDB Search (mongot), so mock + # the collection and verify the pipeline passed to PyMongo. + with patch.object(SearchableDoc, "_collection") as collection: + SearchableDoc.objects(last_name="bar").aggregate([search_step]) + + assert collection.aggregate.call_args.args[0] == [ + search_step, + {"$match": {"last_name": "bar"}}, + ]