We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
The "Did you mean..." suggestions' total is wrong
The "Did you mean..." says there are supposed to be 103 matches on this suggestion:
<img width="503" alt="Screen Shot 2021-02-09 at 5 21 50 PM" src="https://user-images.githubusercontent.com/26739/107436375-6fcd6780-6afb-11eb-8989-a6ea605881eb.png">

But if you apply it, you get a different total:
<img width="458" alt="Screen Shot 2021-02-09 at 5 22 00 PM" src="https://user-images.githubusercontent.com/26739/107436459-8e336300-6afb-11eb-90bf-7e19fdddf08d.png">

Not sure if it's an error in Kuma or in how the results are presented in Yari. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kuma/api/v1/search/__init__.py`
Content:
```
1 from django import http
2 from django.conf import settings
3 from elasticsearch import exceptions
4 from elasticsearch_dsl import Q, query, Search
5 from redo import retrying
6 
7 from kuma.api.v1.decorators import allow_CORS_GET
8 
9 from .forms import SearchForm
10 
11 
12 class JsonResponse(http.JsonResponse):
13     """The only reason this exists is so that other Django views can call
14     views that return instances of this and then get to the data before it
15     gets JSON serialized.
16     This is something that rest_framework's JsonResponse supports.
17     Ultimately, the only view that cares is the (old) Kuma search view page
18     that calls the view function here in this file. Now it can do something like:
19 
20         response = kuma.api.v1.search.search(request)
21         found = response.data
22 
23     """
24 
25     def __init__(self, data, *args, **kwargs):
26         self.data = data
27         super().__init__(data, *args, **kwargs)
28 
29 
30 def legacy(request, locale=None):
31     raise NotImplementedError("work harder")
32 
33 
34 @allow_CORS_GET
35 def search(request, locale=None):
36     initial = {"size": 10, "page": 1, "archive": SearchForm.ARCHIVE_CHOICES[0]}
37     if locale:
38         initial["locale"] = locale
39     form = SearchForm(request.GET, initial=initial)
40     if not form.is_valid():
41         return JsonResponse({"errors": form.errors.get_json_data()}, status=400)
42 
43     locales = form.cleaned_data["locale"] or [settings.LANGUAGE_CODE]
44     assert isinstance(locales, list)
45 
46     params = {
47         "locales": [x.lower() for x in locales],
48         "archive": form.cleaned_data["archive"],
49         "query": form.cleaned_data["q"],
50         "size": form.cleaned_data["size"],
51         "page": form.cleaned_data["page"],
52         "sort": form.cleaned_data["sort"],
53         # The `slug` is always stored, as a Keyword index, in lowercase.
54         "slug_prefixes": [x.lower() for x in form.cleaned_data["slug_prefix"]],
55     }
56     results = _find(
57         params,
58         make_suggestions=True,
59     )
60     return JsonResponse(results)
61 
62 
63 def _find(params, total_only=False, make_suggestions=False, min_suggestion_score=0.8):
64     search_query = Search(
65         index=settings.SEARCH_INDEX_NAME,
66     )
67     if make_suggestions:
68         # XXX research if it it's better to use phrase suggesters and if
69         # that works
70         # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html#phrase-suggester
71         search_query = search_query.suggest(
72             "title_suggestions", params["query"], term={"field": "title"}
73         )
74         search_query = search_query.suggest(
75             "body_suggestions", params["query"], term={"field": "body"}
76         )
77 
78     sub_queries = []
79     sub_queries.append(Q("match", title={"query": params["query"], "boost": 2.0}))
80     sub_queries.append(Q("match", body={"query": params["query"], "boost": 1.0}))
81     if " " in params["query"]:
82         sub_queries.append(
83             Q("match_phrase", title={"query": params["query"], "boost": 10.0})
84         )
85         sub_queries.append(
86             Q("match_phrase", body={"query": params["query"], "boost": 5.0})
87         )
88 
89     sub_query = query.Bool(should=sub_queries)
90 
91     if params["locales"]:
92         search_query = search_query.filter("terms", locale=params["locales"])
93     if params["archive"] == "exclude":
94         search_query = search_query.filter("term", archived=False)
95     elif params["archive"] == "only":
96         search_query = search_query.filter("term", archived=True)
97 
98     if params["slug_prefixes"]:
99         sub_queries = [Q("prefix", slug=x) for x in params["slug_prefixes"]]
100         search_query = search_query.query(query.Bool(should=sub_queries))
101 
102     search_query = search_query.highlight_options(
103         pre_tags=["<mark>"],
104         post_tags=["</mark>"],
105         number_of_fragments=3,
106         fragment_size=120,
107         encoder="html",
108     )
109     search_query = search_query.highlight("title", "body")
110 
111     if params["sort"] == "relevance":
112         search_query = search_query.sort("_score", "-popularity")
113         search_query = search_query.query(sub_query)
114     elif params["sort"] == "popularity":
115         search_query = search_query.sort("-popularity", "_score")
116         search_query = search_query.query(sub_query)
117     else:
118         popularity_factor = 10.0
119         boost_mode = "sum"
120         score_mode = "max"
121         search_query = search_query.query(
122             "function_score",
123             query=sub_query,
124             functions=[
125                 query.SF(
126                     "field_value_factor",
127                     field="popularity",
128                     factor=popularity_factor,
129                     missing=0.0,
130                 )
131             ],
132             boost_mode=boost_mode,
133             score_mode=score_mode,
134         )
135 
136     search_query = search_query.source(excludes=["body"])
137 
138     search_query = search_query[
139         params["size"] * (params["page"] - 1) : params["size"] * params["page"]
140     ]
141 
142     retry_options = {
143         "retry_exceptions": (
144             # This is the standard operational exception.
145             exceptions.ConnectionError,
146             # This can happen if the search happened right as the index had
147             # just been deleted due to a fresh re-indexing happening in Yari.
148             exceptions.NotFoundError,
149             # This can happen when the index simply isn't ready yet.
150             exceptions.TransportError,
151         ),
152         # The default in redo is 60 seconds. Let's tone that down.
153         "sleeptime": settings.ES_RETRY_SLEEPTIME,
154         "attempts": settings.ES_RETRY_ATTEMPTS,
155         "jitter": settings.ES_RETRY_JITTER,
156     }
157     with retrying(search_query.execute, **retry_options) as retrying_function:
158         response = retrying_function()
159 
160     if total_only:
161         return response.hits.total
162 
163     metadata = {
164         "took_ms": response.took,
165         "total": {
166             # The `response.hits.total` is a `elasticsearch_dsl.utils.AttrDict`
167             # instance. Pluck only the exact data needed.
168             "value": response.hits.total.value,
169             "relation": response.hits.total.relation,
170         },
171         "size": params["size"],
172         "page": params["page"],
173     }
174     documents = []
175     for hit in response:
176         try:
177             body_highlight = list(hit.meta.highlight.body)
178         except AttributeError:
179             body_highlight = []
180         try:
181             title_highlight = list(hit.meta.highlight.title)
182         except AttributeError:
183             title_highlight = []
184 
185         d = {
186             "mdn_url": hit.meta.id,
187             "score": hit.meta.score,
188             "title": hit.title,
189             "locale": hit.locale,
190             "slug": hit.slug,
191             "popularity": hit.popularity,
192             "archived": hit.archived,
193             "summary": hit.summary,
194             "highlight": {
195                 "body": body_highlight,
196                 "title": title_highlight,
197             },
198         }
199         documents.append(d)
200 
201     try:
202         suggest = getattr(response, "suggest")
203     except AttributeError:
204         suggest = None
205 
206     suggestions = []
207     if suggest:
208         suggestion_strings = _unpack_suggestions(
209             params["query"],
210             response.suggest,
211             ("body_suggestions", "title_suggestions"),
212         )
213 
214         for score, string in suggestion_strings:
215             if score > min_suggestion_score or 1:
216                 # Sure, this is different way to spell, but what will it yield
217                 # if you actually search it?
218                 total = _find(params, total_only=True)
219                 if total["value"] > 0:
220                     suggestions.append(
221                         {
222                             "text": string,
223                             "total": {
224                                 # This 'total' is an `AttrDict` instance.
225                                 "value": total.value,
226                                 "relation": total.relation,
227                             },
228                         }
229                     )
230                     # Since they're sorted by score, it's usually never useful
231                     # to suggestion more than exactly 1 good suggestion.
232                     break
233 
234     return {
235         "documents": documents,
236         "metadata": metadata,
237         "suggestions": suggestions,
238     }
239 
240 
241 def _unpack_suggestions(query, suggest, keys):
242     alternatives = []
243     for key in keys:
244         for suggestion in getattr(suggest, key, []):
245             for option in suggestion.options:
246                 alternatives.append(
247                     (
248                         option.score,
249                         query[0 : suggestion.offset]
250                         + option.text
251                         + query[suggestion.offset + suggestion.length :],
252                     )
253                 )
254     alternatives.sort(reverse=True)  # highest score first
255     return alternatives
256
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kuma/api/v1/search/__init__.py b/kuma/api/v1/search/__init__.py
--- a/kuma/api/v1/search/__init__.py
+++ b/kuma/api/v1/search/__init__.py
@@ -215,7 +215,7 @@
             if score > min_suggestion_score or 1:
                 # Sure, this is different way to spell, but what will it yield
                 # if you actually search it?
-                total = _find(params, total_only=True)
+                total = _find(dict(params, query=string), total_only=True)
                 if total["value"] > 0:
                     suggestions.append(
                         {

{"golden_diff": "diff --git a/kuma/api/v1/search/__init__.py b/kuma/api/v1/search/__init__.py\n--- a/kuma/api/v1/search/__init__.py\n+++ b/kuma/api/v1/search/__init__.py\n@@ -215,7 +215,7 @@\n             if score > min_suggestion_score or 1:\n                 # Sure, this is different way to spell, but what will it yield\n                 # if you actually search it?\n-                total = _find(params, total_only=True)\n+                total = _find(dict(params, query=string), total_only=True)\n                 if total[\"value\"] > 0:\n                     suggestions.append(\n                         {\n", "issue": "The \"Did you mean...\" suggestions' total is wrong\nThe \"Did you mean...\" says there are supposed to be 103 matches on this suggestion:\r\n<img width=\"503\" alt=\"Screen Shot 2021-02-09 at 5 21 50 PM\" src=\"https://user-images.githubusercontent.com/26739/107436375-6fcd6780-6afb-11eb-8989-a6ea605881eb.png\">\r\n\r\nBut if you apply it, you get a different total:\r\n<img width=\"458\" alt=\"Screen Shot 2021-02-09 at 5 22 00 PM\" src=\"https://user-images.githubusercontent.com/26739/107436459-8e336300-6afb-11eb-90bf-7e19fdddf08d.png\">\r\n\r\nNot sure if it's an error in Kuma or in how the results are presented in Yari. \r\n\n", "before_files": [{"content": "from django import http\nfrom django.conf import settings\nfrom elasticsearch import exceptions\nfrom elasticsearch_dsl import Q, query, Search\nfrom redo import retrying\n\nfrom kuma.api.v1.decorators import allow_CORS_GET\n\nfrom .forms import SearchForm\n\n\nclass JsonResponse(http.JsonResponse):\n    \"\"\"The only reason this exists is so that other Django views can call\n    views that return instances of this and then get to the data before it\n    gets JSON serialized.\n    This is something that rest_framework's JsonResponse supports.\n    Ultimately, the only view that cares is the (old) Kuma search view page\n    that calls the view function here in this file. Now it can do something like:\n\n        response = kuma.api.v1.search.search(request)\n        found = response.data\n\n    \"\"\"\n\n    def __init__(self, data, *args, **kwargs):\n        self.data = data\n        super().__init__(data, *args, **kwargs)\n\n\ndef legacy(request, locale=None):\n    raise NotImplementedError(\"work harder\")\n\n\n@allow_CORS_GET\ndef search(request, locale=None):\n    initial = {\"size\": 10, \"page\": 1, \"archive\": SearchForm.ARCHIVE_CHOICES[0]}\n    if locale:\n        initial[\"locale\"] = locale\n    form = SearchForm(request.GET, initial=initial)\n    if not form.is_valid():\n        return JsonResponse({\"errors\": form.errors.get_json_data()}, status=400)\n\n    locales = form.cleaned_data[\"locale\"] or [settings.LANGUAGE_CODE]\n    assert isinstance(locales, list)\n\n    params = {\n        \"locales\": [x.lower() for x in locales],\n        \"archive\": form.cleaned_data[\"archive\"],\n        \"query\": form.cleaned_data[\"q\"],\n        \"size\": form.cleaned_data[\"size\"],\n        \"page\": form.cleaned_data[\"page\"],\n        \"sort\": form.cleaned_data[\"sort\"],\n        # The `slug` is always stored, as a Keyword index, in lowercase.\n        \"slug_prefixes\": [x.lower() for x in form.cleaned_data[\"slug_prefix\"]],\n    }\n    results = _find(\n        params,\n        make_suggestions=True,\n    )\n    return JsonResponse(results)\n\n\ndef _find(params, total_only=False, make_suggestions=False, min_suggestion_score=0.8):\n    search_query = Search(\n        index=settings.SEARCH_INDEX_NAME,\n    )\n    if make_suggestions:\n        # XXX research if it it's better to use phrase suggesters and if\n        # that works\n        # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html#phrase-suggester\n        search_query = search_query.suggest(\n            \"title_suggestions\", params[\"query\"], term={\"field\": \"title\"}\n        )\n        search_query = search_query.suggest(\n            \"body_suggestions\", params[\"query\"], term={\"field\": \"body\"}\n        )\n\n    sub_queries = []\n    sub_queries.append(Q(\"match\", title={\"query\": params[\"query\"], \"boost\": 2.0}))\n    sub_queries.append(Q(\"match\", body={\"query\": params[\"query\"], \"boost\": 1.0}))\n    if \" \" in params[\"query\"]:\n        sub_queries.append(\n            Q(\"match_phrase\", title={\"query\": params[\"query\"], \"boost\": 10.0})\n        )\n        sub_queries.append(\n            Q(\"match_phrase\", body={\"query\": params[\"query\"], \"boost\": 5.0})\n        )\n\n    sub_query = query.Bool(should=sub_queries)\n\n    if params[\"locales\"]:\n        search_query = search_query.filter(\"terms\", locale=params[\"locales\"])\n    if params[\"archive\"] == \"exclude\":\n        search_query = search_query.filter(\"term\", archived=False)\n    elif params[\"archive\"] == \"only\":\n        search_query = search_query.filter(\"term\", archived=True)\n\n    if params[\"slug_prefixes\"]:\n        sub_queries = [Q(\"prefix\", slug=x) for x in params[\"slug_prefixes\"]]\n        search_query = search_query.query(query.Bool(should=sub_queries))\n\n    search_query = search_query.highlight_options(\n        pre_tags=[\"<mark>\"],\n        post_tags=[\"</mark>\"],\n        number_of_fragments=3,\n        fragment_size=120,\n        encoder=\"html\",\n    )\n    search_query = search_query.highlight(\"title\", \"body\")\n\n    if params[\"sort\"] == \"relevance\":\n        search_query = search_query.sort(\"_score\", \"-popularity\")\n        search_query = search_query.query(sub_query)\n    elif params[\"sort\"] == \"popularity\":\n        search_query = search_query.sort(\"-popularity\", \"_score\")\n        search_query = search_query.query(sub_query)\n    else:\n        popularity_factor = 10.0\n        boost_mode = \"sum\"\n        score_mode = \"max\"\n        search_query = search_query.query(\n            \"function_score\",\n            query=sub_query,\n            functions=[\n                query.SF(\n                    \"field_value_factor\",\n                    field=\"popularity\",\n                    factor=popularity_factor,\n                    missing=0.0,\n                )\n            ],\n            boost_mode=boost_mode,\n            score_mode=score_mode,\n        )\n\n    search_query = search_query.source(excludes=[\"body\"])\n\n    search_query = search_query[\n        params[\"size\"] * (params[\"page\"] - 1) : params[\"size\"] * params[\"page\"]\n    ]\n\n    retry_options = {\n        \"retry_exceptions\": (\n            # This is the standard operational exception.\n            exceptions.ConnectionError,\n            # This can happen if the search happened right as the index had\n            # just been deleted due to a fresh re-indexing happening in Yari.\n            exceptions.NotFoundError,\n            # This can happen when the index simply isn't ready yet.\n            exceptions.TransportError,\n        ),\n        # The default in redo is 60 seconds. Let's tone that down.\n        \"sleeptime\": settings.ES_RETRY_SLEEPTIME,\n        \"attempts\": settings.ES_RETRY_ATTEMPTS,\n        \"jitter\": settings.ES_RETRY_JITTER,\n    }\n    with retrying(search_query.execute, **retry_options) as retrying_function:\n        response = retrying_function()\n\n    if total_only:\n        return response.hits.total\n\n    metadata = {\n        \"took_ms\": response.took,\n        \"total\": {\n            # The `response.hits.total` is a `elasticsearch_dsl.utils.AttrDict`\n            # instance. Pluck only the exact data needed.\n            \"value\": response.hits.total.value,\n            \"relation\": response.hits.total.relation,\n        },\n        \"size\": params[\"size\"],\n        \"page\": params[\"page\"],\n    }\n    documents = []\n    for hit in response:\n        try:\n            body_highlight = list(hit.meta.highlight.body)\n        except AttributeError:\n            body_highlight = []\n        try:\n            title_highlight = list(hit.meta.highlight.title)\n        except AttributeError:\n            title_highlight = []\n\n        d = {\n            \"mdn_url\": hit.meta.id,\n            \"score\": hit.meta.score,\n            \"title\": hit.title,\n            \"locale\": hit.locale,\n            \"slug\": hit.slug,\n            \"popularity\": hit.popularity,\n            \"archived\": hit.archived,\n            \"summary\": hit.summary,\n            \"highlight\": {\n                \"body\": body_highlight,\n                \"title\": title_highlight,\n            },\n        }\n        documents.append(d)\n\n    try:\n        suggest = getattr(response, \"suggest\")\n    except AttributeError:\n        suggest = None\n\n    suggestions = []\n    if suggest:\n        suggestion_strings = _unpack_suggestions(\n            params[\"query\"],\n            response.suggest,\n            (\"body_suggestions\", \"title_suggestions\"),\n        )\n\n        for score, string in suggestion_strings:\n            if score > min_suggestion_score or 1:\n                # Sure, this is different way to spell, but what will it yield\n                # if you actually search it?\n                total = _find(params, total_only=True)\n                if total[\"value\"] > 0:\n                    suggestions.append(\n                        {\n                            \"text\": string,\n                            \"total\": {\n                                # This 'total' is an `AttrDict` instance.\n                                \"value\": total.value,\n                                \"relation\": total.relation,\n                            },\n                        }\n                    )\n                    # Since they're sorted by score, it's usually never useful\n                    # to suggestion more than exactly 1 good suggestion.\n                    break\n\n    return {\n        \"documents\": documents,\n        \"metadata\": metadata,\n        \"suggestions\": suggestions,\n    }\n\n\ndef _unpack_suggestions(query, suggest, keys):\n    alternatives = []\n    for key in keys:\n        for suggestion in getattr(suggest, key, []):\n            for option in suggestion.options:\n                alternatives.append(\n                    (\n                        option.score,\n                        query[0 : suggestion.offset]\n                        + option.text\n                        + query[suggestion.offset + suggestion.length :],\n                    )\n                )\n    alternatives.sort(reverse=True)  # highest score first\n    return alternatives\n", "path": "kuma/api/v1/search/__init__.py"}], "after_files": [{"content": "from django import http\nfrom django.conf import settings\nfrom elasticsearch import exceptions\nfrom elasticsearch_dsl import Q, query, Search\nfrom redo import retrying\n\nfrom kuma.api.v1.decorators import allow_CORS_GET\n\nfrom .forms import SearchForm\n\n\nclass JsonResponse(http.JsonResponse):\n    \"\"\"The only reason this exists is so that other Django views can call\n    views that return instances of this and then get to the data before it\n    gets JSON serialized.\n    This is something that rest_framework's JsonResponse supports.\n    Ultimately, the only view that cares is the (old) Kuma search view page\n    that calls the view function here in this file. Now it can do something like:\n\n        response = kuma.api.v1.search.search(request)\n        found = response.data\n\n    \"\"\"\n\n    def __init__(self, data, *args, **kwargs):\n        self.data = data\n        super().__init__(data, *args, **kwargs)\n\n\ndef legacy(request, locale=None):\n    raise NotImplementedError(\"work harder\")\n\n\n@allow_CORS_GET\ndef search(request, locale=None):\n    initial = {\"size\": 10, \"page\": 1, \"archive\": SearchForm.ARCHIVE_CHOICES[0]}\n    if locale:\n        initial[\"locale\"] = locale\n    form = SearchForm(request.GET, initial=initial)\n    if not form.is_valid():\n        return JsonResponse({\"errors\": form.errors.get_json_data()}, status=400)\n\n    locales = form.cleaned_data[\"locale\"] or [settings.LANGUAGE_CODE]\n    assert isinstance(locales, list)\n\n    params = {\n        \"locales\": [x.lower() for x in locales],\n        \"archive\": form.cleaned_data[\"archive\"],\n        \"query\": form.cleaned_data[\"q\"],\n        \"size\": form.cleaned_data[\"size\"],\n        \"page\": form.cleaned_data[\"page\"],\n        \"sort\": form.cleaned_data[\"sort\"],\n        # The `slug` is always stored, as a Keyword index, in lowercase.\n        \"slug_prefixes\": [x.lower() for x in form.cleaned_data[\"slug_prefix\"]],\n    }\n    results = _find(\n        params,\n        make_suggestions=True,\n    )\n    return JsonResponse(results)\n\n\ndef _find(params, total_only=False, make_suggestions=False, min_suggestion_score=0.8):\n    search_query = Search(\n        index=settings.SEARCH_INDEX_NAME,\n    )\n    if make_suggestions:\n        # XXX research if it it's better to use phrase suggesters and if\n        # that works\n        # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html#phrase-suggester\n        search_query = search_query.suggest(\n            \"title_suggestions\", params[\"query\"], term={\"field\": \"title\"}\n        )\n        search_query = search_query.suggest(\n            \"body_suggestions\", params[\"query\"], term={\"field\": \"body\"}\n        )\n\n    sub_queries = []\n    sub_queries.append(Q(\"match\", title={\"query\": params[\"query\"], \"boost\": 2.0}))\n    sub_queries.append(Q(\"match\", body={\"query\": params[\"query\"], \"boost\": 1.0}))\n    if \" \" in params[\"query\"]:\n        sub_queries.append(\n            Q(\"match_phrase\", title={\"query\": params[\"query\"], \"boost\": 10.0})\n        )\n        sub_queries.append(\n            Q(\"match_phrase\", body={\"query\": params[\"query\"], \"boost\": 5.0})\n        )\n\n    sub_query = query.Bool(should=sub_queries)\n\n    if params[\"locales\"]:\n        search_query = search_query.filter(\"terms\", locale=params[\"locales\"])\n    if params[\"archive\"] == \"exclude\":\n        search_query = search_query.filter(\"term\", archived=False)\n    elif params[\"archive\"] == \"only\":\n        search_query = search_query.filter(\"term\", archived=True)\n\n    if params[\"slug_prefixes\"]:\n        sub_queries = [Q(\"prefix\", slug=x) for x in params[\"slug_prefixes\"]]\n        search_query = search_query.query(query.Bool(should=sub_queries))\n\n    search_query = search_query.highlight_options(\n        pre_tags=[\"<mark>\"],\n        post_tags=[\"</mark>\"],\n        number_of_fragments=3,\n        fragment_size=120,\n        encoder=\"html\",\n    )\n    search_query = search_query.highlight(\"title\", \"body\")\n\n    if params[\"sort\"] == \"relevance\":\n        search_query = search_query.sort(\"_score\", \"-popularity\")\n        search_query = search_query.query(sub_query)\n    elif params[\"sort\"] == \"popularity\":\n        search_query = search_query.sort(\"-popularity\", \"_score\")\n        search_query = search_query.query(sub_query)\n    else:\n        popularity_factor = 10.0\n        boost_mode = \"sum\"\n        score_mode = \"max\"\n        search_query = search_query.query(\n            \"function_score\",\n            query=sub_query,\n            functions=[\n                query.SF(\n                    \"field_value_factor\",\n                    field=\"popularity\",\n                    factor=popularity_factor,\n                    missing=0.0,\n                )\n            ],\n            boost_mode=boost_mode,\n            score_mode=score_mode,\n        )\n\n    search_query = search_query.source(excludes=[\"body\"])\n\n    search_query = search_query[\n        params[\"size\"] * (params[\"page\"] - 1) : params[\"size\"] * params[\"page\"]\n    ]\n\n    retry_options = {\n        \"retry_exceptions\": (\n            # This is the standard operational exception.\n            exceptions.ConnectionError,\n            # This can happen if the search happened right as the index had\n            # just been deleted due to a fresh re-indexing happening in Yari.\n            exceptions.NotFoundError,\n            # This can happen when the index simply isn't ready yet.\n            exceptions.TransportError,\n        ),\n        # The default in redo is 60 seconds. Let's tone that down.\n        \"sleeptime\": settings.ES_RETRY_SLEEPTIME,\n        \"attempts\": settings.ES_RETRY_ATTEMPTS,\n        \"jitter\": settings.ES_RETRY_JITTER,\n    }\n    with retrying(search_query.execute, **retry_options) as retrying_function:\n        response = retrying_function()\n\n    if total_only:\n        return response.hits.total\n\n    metadata = {\n        \"took_ms\": response.took,\n        \"total\": {\n            # The `response.hits.total` is a `elasticsearch_dsl.utils.AttrDict`\n            # instance. Pluck only the exact data needed.\n            \"value\": response.hits.total.value,\n            \"relation\": response.hits.total.relation,\n        },\n        \"size\": params[\"size\"],\n        \"page\": params[\"page\"],\n    }\n    documents = []\n    for hit in response:\n        try:\n            body_highlight = list(hit.meta.highlight.body)\n        except AttributeError:\n            body_highlight = []\n        try:\n            title_highlight = list(hit.meta.highlight.title)\n        except AttributeError:\n            title_highlight = []\n\n        d = {\n            \"mdn_url\": hit.meta.id,\n            \"score\": hit.meta.score,\n            \"title\": hit.title,\n            \"locale\": hit.locale,\n            \"slug\": hit.slug,\n            \"popularity\": hit.popularity,\n            \"archived\": hit.archived,\n            \"summary\": hit.summary,\n            \"highlight\": {\n                \"body\": body_highlight,\n                \"title\": title_highlight,\n            },\n        }\n        documents.append(d)\n\n    try:\n        suggest = getattr(response, \"suggest\")\n    except AttributeError:\n        suggest = None\n\n    suggestions = []\n    if suggest:\n        suggestion_strings = _unpack_suggestions(\n            params[\"query\"],\n            response.suggest,\n            (\"body_suggestions\", \"title_suggestions\"),\n        )\n\n        for score, string in suggestion_strings:\n            if score > min_suggestion_score or 1:\n                # Sure, this is different way to spell, but what will it yield\n                # if you actually search it?\n                total = _find(dict(params, query=string), total_only=True)\n                if total[\"value\"] > 0:\n                    suggestions.append(\n                        {\n                            \"text\": string,\n                            \"total\": {\n                                # This 'total' is an `AttrDict` instance.\n                                \"value\": total.value,\n                                \"relation\": total.relation,\n                            },\n                        }\n                    )\n                    # Since they're sorted by score, it's usually never useful\n                    # to suggestion more than exactly 1 good suggestion.\n                    break\n\n    return {\n        \"documents\": documents,\n        \"metadata\": metadata,\n        \"suggestions\": suggestions,\n    }\n\n\ndef _unpack_suggestions(query, suggest, keys):\n    alternatives = []\n    for key in keys:\n        for suggestion in getattr(suggest, key, []):\n            for option in suggestion.options:\n                alternatives.append(\n                    (\n                        option.score,\n                        query[0 : suggestion.offset]\n                        + option.text\n                        + query[suggestion.offset + suggestion.length :],\n                    )\n                )\n    alternatives.sort(reverse=True)  # highest score first\n    return alternatives\n", "path": "kuma/api/v1/search/__init__.py"}]}