We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Hyphens in search query are normalized differently than in ElasticSearch
If I have the substring "fooo-baar" in the text of one of the indexed fields for a Page-derived model instance, I'd expect to be see that page when I search for "fooo-baar", but I don't.

This seems to be because `wagtailsearch` normalizes "fooo-baar" to "fooobaar", while ElasticSearch treats the hyphen as a whitespace character.

Failing test: add 

```
"Hello-world",
```

to `test_queries:45`.

Suggested fix: normalize to "fooo baar" instead.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/wagtailsearch/backends/base.py`
Content:
```
1 from six import text_type
2 
3 from django.db import models
4 from django.db.models.query import QuerySet
5 from django.db.models.lookups import Lookup
6 from django.db.models.sql.where import SubqueryConstraint, WhereNode
7 from django.core.exceptions import ImproperlyConfigured
8 
9 from wagtail.wagtailsearch.index import class_is_indexed
10 from wagtail.wagtailsearch.utils import normalise_query_string
11 
12 
13 class FilterError(Exception):
14     pass
15 
16 
17 class FieldError(Exception):
18     pass
19 
20 
21 class BaseSearchQuery(object):
22     def __init__(self, queryset, query_string, fields=None):
23         self.queryset = queryset
24         self.query_string = query_string
25         self.fields = fields
26 
27     def _get_searchable_field(self, field_attname):
28         # Get field
29         field = dict(
30             (field.get_attname(self.queryset.model), field)
31             for field in self.queryset.model.get_searchable_search_fields()
32         ).get(field_attname, None)
33 
34         return field
35 
36     def _get_filterable_field(self, field_attname):
37         # Get field
38         field = dict(
39             (field.get_attname(self.queryset.model), field)
40             for field in self.queryset.model.get_filterable_search_fields()
41         ).get(field_attname, None)
42 
43         return field
44 
45     def _process_lookup(self, field, lookup, value):
46         raise NotImplementedError
47 
48     def _connect_filters(self, filters, connector, negated):
49         raise NotImplementedError
50 
51     def _process_filter(self, field_attname, lookup, value):
52         # Get the field
53         field = self._get_filterable_field(field_attname)
54 
55         if field is None:
56             raise FieldError('Cannot filter search results with field "' + field_attname + '". Please add index.FilterField(\'' + field_attname + '\') to ' + self.queryset.model.__name__ + '.search_fields.')
57 
58         # Process the lookup
59         result = self._process_lookup(field, lookup, value)
60 
61         if result is None:
62             raise FilterError('Could not apply filter on search results: "' + field_attname + '__' + lookup + ' = ' + text_type(value) + '". Lookup "' + lookup + '"" not recognosed.')
63 
64         return result
65 
66     def _get_filters_from_where_node(self, where_node):
67         # Check if this is a leaf node
68         if isinstance(where_node, Lookup):
69             field_attname = where_node.lhs.target.attname
70             lookup = where_node.lookup_name
71             value = where_node.rhs
72 
73             # Process the filter
74             return self._process_filter(field_attname, lookup, value)
75 
76         elif isinstance(where_node, SubqueryConstraint):
77             raise FilterError('Could not apply filter on search results: Subqueries are not allowed.')
78 
79         elif isinstance(where_node, WhereNode):
80             # Get child filters
81             connector = where_node.connector
82             child_filters = [self._get_filters_from_where_node(child) for child in where_node.children]
83             child_filters = [child_filter for child_filter in child_filters if child_filter]
84 
85             return self._connect_filters(child_filters, connector, where_node.negated)
86 
87         else:
88             raise FilterError('Could not apply filter on search results: Unknown where node: ' + str(type(where_node)))
89 
90     def _get_filters_from_queryset(self):
91         return self._get_filters_from_where_node(self.queryset.query.where)
92 
93 
94 class BaseSearchResults(object):
95     def __init__(self, backend, query, prefetch_related=None):
96         self.backend = backend
97         self.query = query
98         self.prefetch_related = prefetch_related
99         self.start = 0
100         self.stop = None
101         self._results_cache = None
102         self._count_cache = None
103 
104     def _set_limits(self, start=None, stop=None):
105         if stop is not None:
106             if self.stop is not None:
107                 self.stop = min(self.stop, self.start + stop)
108             else:
109                 self.stop = self.start + stop
110 
111         if start is not None:
112             if self.stop is not None:
113                 self.start = min(self.stop, self.start + start)
114             else:
115                 self.start = self.start + start
116 
117     def _clone(self):
118         klass = self.__class__
119         new = klass(self.backend, self.query, prefetch_related=self.prefetch_related)
120         new.start = self.start
121         new.stop = self.stop
122         return new
123 
124     def _do_search(self):
125         raise NotImplementedError
126 
127     def _do_count(self):
128         raise NotImplementedError
129 
130     def results(self):
131         if self._results_cache is None:
132             self._results_cache = self._do_search()
133         return self._results_cache
134 
135     def count(self):
136         if self._count_cache is None:
137             if self._results_cache is not None:
138                 self._count_cache = len(self._results_cache)
139             else:
140                 self._count_cache = self._do_count()
141         return self._count_cache
142 
143     def __getitem__(self, key):
144         new = self._clone()
145 
146         if isinstance(key, slice):
147             # Set limits
148             start = int(key.start) if key.start else None
149             stop = int(key.stop) if key.stop else None
150             new._set_limits(start, stop)
151 
152             # Copy results cache
153             if self._results_cache is not None:
154                 new._results_cache = self._results_cache[key]
155 
156             return new
157         else:
158             if self._results_cache is not None:
159                 return self._results_cache[key]
160 
161             new.start = key
162             new.stop = key + 1
163             return list(new)[0]
164 
165     def __iter__(self):
166         return iter(self.results())
167 
168     def __len__(self):
169         return len(self.results())
170 
171     def __repr__(self):
172         data = list(self[:21])
173         if len(data) > 20:
174             data[-1] = "...(remaining elements truncated)..."
175         return repr(data)
176 
177 
178 class BaseSearch(object):
179     def __init__(self, params):
180         pass
181 
182     def reset_index(self):
183         raise NotImplementedError
184 
185     def add_type(self, model):
186         raise NotImplementedError
187 
188     def refresh_index(self):
189         raise NotImplementedError
190 
191     def add(self, obj):
192         raise NotImplementedError
193 
194     def add_bulk(self, model, obj_list):
195         raise NotImplementedError
196 
197     def delete(self, obj):
198         raise NotImplementedError
199 
200     def _search(self, queryset, query_string, fields=None):
201         raise NotImplementedError
202 
203     def search(self, query_string, model_or_queryset, fields=None, filters=None, prefetch_related=None):
204         # Find model/queryset
205         if isinstance(model_or_queryset, QuerySet):
206             model = model_or_queryset.model
207             queryset = model_or_queryset
208         else:
209             model = model_or_queryset
210             queryset = model_or_queryset.objects.all()
211 
212         # Model must be a class that is in the index
213         if not class_is_indexed(model):
214             return []
215 
216         # Normalise query string
217         if query_string is not None:
218             query_string = normalise_query_string(query_string)
219 
220         # Check that theres still a query string after the clean up
221         if query_string == "":
222             return []
223 
224         # Apply filters to queryset
225         if filters:
226             queryset = queryset.filter(**filters)
227 
228         # Prefetch related
229         if prefetch_related:
230             for prefetch in prefetch_related:
231                 queryset = queryset.prefetch_related(prefetch)
232 
233         # Search
234         return self._search(queryset, query_string, fields=fields)
235
```
--- 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/wagtail/wagtailsearch/backends/base.py b/wagtail/wagtailsearch/backends/base.py
--- a/wagtail/wagtailsearch/backends/base.py
+++ b/wagtail/wagtailsearch/backends/base.py
@@ -7,7 +7,6 @@
 from django.core.exceptions import ImproperlyConfigured
 
 from wagtail.wagtailsearch.index import class_is_indexed
-from wagtail.wagtailsearch.utils import normalise_query_string
 
 
 class FilterError(Exception):
@@ -213,10 +212,6 @@
         if not class_is_indexed(model):
             return []
 
-        # Normalise query string
-        if query_string is not None:
-            query_string = normalise_query_string(query_string)
-
         # Check that theres still a query string after the clean up
         if query_string == "":
             return []

{"golden_diff": "diff --git a/wagtail/wagtailsearch/backends/base.py b/wagtail/wagtailsearch/backends/base.py\n--- a/wagtail/wagtailsearch/backends/base.py\n+++ b/wagtail/wagtailsearch/backends/base.py\n@@ -7,7 +7,6 @@\n from django.core.exceptions import ImproperlyConfigured\n \n from wagtail.wagtailsearch.index import class_is_indexed\n-from wagtail.wagtailsearch.utils import normalise_query_string\n \n \n class FilterError(Exception):\n@@ -213,10 +212,6 @@\n         if not class_is_indexed(model):\n             return []\n \n-        # Normalise query string\n-        if query_string is not None:\n-            query_string = normalise_query_string(query_string)\n-\n         # Check that theres still a query string after the clean up\n         if query_string == \"\":\n             return []\n", "issue": "Hyphens in search query are normalized differently than in ElasticSearch\nIf I have the substring \"fooo-baar\" in the text of one of the indexed fields for a Page-derived model instance, I'd expect to be see that page when I search for \"fooo-baar\", but I don't.\n\nThis seems to be because `wagtailsearch` normalizes \"fooo-baar\" to \"fooobaar\", while ElasticSearch treats the hyphen as a whitespace character.\n\nFailing test: add \n\n```\n\"Hello-world\",\n```\n\nto `test_queries:45`.\n\nSuggested fix: normalize to \"fooo baar\" instead.\n\n", "before_files": [{"content": "from six import text_type\n\nfrom django.db import models\nfrom django.db.models.query import QuerySet\nfrom django.db.models.lookups import Lookup\nfrom django.db.models.sql.where import SubqueryConstraint, WhereNode\nfrom django.core.exceptions import ImproperlyConfigured\n\nfrom wagtail.wagtailsearch.index import class_is_indexed\nfrom wagtail.wagtailsearch.utils import normalise_query_string\n\n\nclass FilterError(Exception):\n    pass\n\n\nclass FieldError(Exception):\n    pass\n\n\nclass BaseSearchQuery(object):\n    def __init__(self, queryset, query_string, fields=None):\n        self.queryset = queryset\n        self.query_string = query_string\n        self.fields = fields\n\n    def _get_searchable_field(self, field_attname):\n        # Get field\n        field = dict(\n            (field.get_attname(self.queryset.model), field)\n            for field in self.queryset.model.get_searchable_search_fields()\n        ).get(field_attname, None)\n\n        return field\n\n    def _get_filterable_field(self, field_attname):\n        # Get field\n        field = dict(\n            (field.get_attname(self.queryset.model), field)\n            for field in self.queryset.model.get_filterable_search_fields()\n        ).get(field_attname, None)\n\n        return field\n\n    def _process_lookup(self, field, lookup, value):\n        raise NotImplementedError\n\n    def _connect_filters(self, filters, connector, negated):\n        raise NotImplementedError\n\n    def _process_filter(self, field_attname, lookup, value):\n        # Get the field\n        field = self._get_filterable_field(field_attname)\n\n        if field is None:\n            raise FieldError('Cannot filter search results with field \"' + field_attname + '\". Please add index.FilterField(\\'' + field_attname + '\\') to ' + self.queryset.model.__name__ + '.search_fields.')\n\n        # Process the lookup\n        result = self._process_lookup(field, lookup, value)\n\n        if result is None:\n            raise FilterError('Could not apply filter on search results: \"' + field_attname + '__' + lookup + ' = ' + text_type(value) + '\". Lookup \"' + lookup + '\"\" not recognosed.')\n\n        return result\n\n    def _get_filters_from_where_node(self, where_node):\n        # Check if this is a leaf node\n        if isinstance(where_node, Lookup):\n            field_attname = where_node.lhs.target.attname\n            lookup = where_node.lookup_name\n            value = where_node.rhs\n\n            # Process the filter\n            return self._process_filter(field_attname, lookup, value)\n\n        elif isinstance(where_node, SubqueryConstraint):\n            raise FilterError('Could not apply filter on search results: Subqueries are not allowed.')\n\n        elif isinstance(where_node, WhereNode):\n            # Get child filters\n            connector = where_node.connector\n            child_filters = [self._get_filters_from_where_node(child) for child in where_node.children]\n            child_filters = [child_filter for child_filter in child_filters if child_filter]\n\n            return self._connect_filters(child_filters, connector, where_node.negated)\n\n        else:\n            raise FilterError('Could not apply filter on search results: Unknown where node: ' + str(type(where_node)))\n\n    def _get_filters_from_queryset(self):\n        return self._get_filters_from_where_node(self.queryset.query.where)\n\n\nclass BaseSearchResults(object):\n    def __init__(self, backend, query, prefetch_related=None):\n        self.backend = backend\n        self.query = query\n        self.prefetch_related = prefetch_related\n        self.start = 0\n        self.stop = None\n        self._results_cache = None\n        self._count_cache = None\n\n    def _set_limits(self, start=None, stop=None):\n        if stop is not None:\n            if self.stop is not None:\n                self.stop = min(self.stop, self.start + stop)\n            else:\n                self.stop = self.start + stop\n\n        if start is not None:\n            if self.stop is not None:\n                self.start = min(self.stop, self.start + start)\n            else:\n                self.start = self.start + start\n\n    def _clone(self):\n        klass = self.__class__\n        new = klass(self.backend, self.query, prefetch_related=self.prefetch_related)\n        new.start = self.start\n        new.stop = self.stop\n        return new\n\n    def _do_search(self):\n        raise NotImplementedError\n\n    def _do_count(self):\n        raise NotImplementedError\n\n    def results(self):\n        if self._results_cache is None:\n            self._results_cache = self._do_search()\n        return self._results_cache\n\n    def count(self):\n        if self._count_cache is None:\n            if self._results_cache is not None:\n                self._count_cache = len(self._results_cache)\n            else:\n                self._count_cache = self._do_count()\n        return self._count_cache\n\n    def __getitem__(self, key):\n        new = self._clone()\n\n        if isinstance(key, slice):\n            # Set limits\n            start = int(key.start) if key.start else None\n            stop = int(key.stop) if key.stop else None\n            new._set_limits(start, stop)\n\n            # Copy results cache\n            if self._results_cache is not None:\n                new._results_cache = self._results_cache[key]\n\n            return new\n        else:\n            if self._results_cache is not None:\n                return self._results_cache[key]\n\n            new.start = key\n            new.stop = key + 1\n            return list(new)[0]\n\n    def __iter__(self):\n        return iter(self.results())\n\n    def __len__(self):\n        return len(self.results())\n\n    def __repr__(self):\n        data = list(self[:21])\n        if len(data) > 20:\n            data[-1] = \"...(remaining elements truncated)...\"\n        return repr(data)\n\n\nclass BaseSearch(object):\n    def __init__(self, params):\n        pass\n\n    def reset_index(self):\n        raise NotImplementedError\n\n    def add_type(self, model):\n        raise NotImplementedError\n\n    def refresh_index(self):\n        raise NotImplementedError\n\n    def add(self, obj):\n        raise NotImplementedError\n\n    def add_bulk(self, model, obj_list):\n        raise NotImplementedError\n\n    def delete(self, obj):\n        raise NotImplementedError\n\n    def _search(self, queryset, query_string, fields=None):\n        raise NotImplementedError\n\n    def search(self, query_string, model_or_queryset, fields=None, filters=None, prefetch_related=None):\n        # Find model/queryset\n        if isinstance(model_or_queryset, QuerySet):\n            model = model_or_queryset.model\n            queryset = model_or_queryset\n        else:\n            model = model_or_queryset\n            queryset = model_or_queryset.objects.all()\n\n        # Model must be a class that is in the index\n        if not class_is_indexed(model):\n            return []\n\n        # Normalise query string\n        if query_string is not None:\n            query_string = normalise_query_string(query_string)\n\n        # Check that theres still a query string after the clean up\n        if query_string == \"\":\n            return []\n\n        # Apply filters to queryset\n        if filters:\n            queryset = queryset.filter(**filters)\n\n        # Prefetch related\n        if prefetch_related:\n            for prefetch in prefetch_related:\n                queryset = queryset.prefetch_related(prefetch)\n\n        # Search\n        return self._search(queryset, query_string, fields=fields)\n", "path": "wagtail/wagtailsearch/backends/base.py"}], "after_files": [{"content": "from six import text_type\n\nfrom django.db import models\nfrom django.db.models.query import QuerySet\nfrom django.db.models.lookups import Lookup\nfrom django.db.models.sql.where import SubqueryConstraint, WhereNode\nfrom django.core.exceptions import ImproperlyConfigured\n\nfrom wagtail.wagtailsearch.index import class_is_indexed\n\n\nclass FilterError(Exception):\n    pass\n\n\nclass FieldError(Exception):\n    pass\n\n\nclass BaseSearchQuery(object):\n    def __init__(self, queryset, query_string, fields=None):\n        self.queryset = queryset\n        self.query_string = query_string\n        self.fields = fields\n\n    def _get_searchable_field(self, field_attname):\n        # Get field\n        field = dict(\n            (field.get_attname(self.queryset.model), field)\n            for field in self.queryset.model.get_searchable_search_fields()\n        ).get(field_attname, None)\n\n        return field\n\n    def _get_filterable_field(self, field_attname):\n        # Get field\n        field = dict(\n            (field.get_attname(self.queryset.model), field)\n            for field in self.queryset.model.get_filterable_search_fields()\n        ).get(field_attname, None)\n\n        return field\n\n    def _process_lookup(self, field, lookup, value):\n        raise NotImplementedError\n\n    def _connect_filters(self, filters, connector, negated):\n        raise NotImplementedError\n\n    def _process_filter(self, field_attname, lookup, value):\n        # Get the field\n        field = self._get_filterable_field(field_attname)\n\n        if field is None:\n            raise FieldError('Cannot filter search results with field \"' + field_attname + '\". Please add index.FilterField(\\'' + field_attname + '\\') to ' + self.queryset.model.__name__ + '.search_fields.')\n\n        # Process the lookup\n        result = self._process_lookup(field, lookup, value)\n\n        if result is None:\n            raise FilterError('Could not apply filter on search results: \"' + field_attname + '__' + lookup + ' = ' + text_type(value) + '\". Lookup \"' + lookup + '\"\" not recognosed.')\n\n        return result\n\n    def _get_filters_from_where_node(self, where_node):\n        # Check if this is a leaf node\n        if isinstance(where_node, Lookup):\n            field_attname = where_node.lhs.target.attname\n            lookup = where_node.lookup_name\n            value = where_node.rhs\n\n            # Process the filter\n            return self._process_filter(field_attname, lookup, value)\n\n        elif isinstance(where_node, SubqueryConstraint):\n            raise FilterError('Could not apply filter on search results: Subqueries are not allowed.')\n\n        elif isinstance(where_node, WhereNode):\n            # Get child filters\n            connector = where_node.connector\n            child_filters = [self._get_filters_from_where_node(child) for child in where_node.children]\n            child_filters = [child_filter for child_filter in child_filters if child_filter]\n\n            return self._connect_filters(child_filters, connector, where_node.negated)\n\n        else:\n            raise FilterError('Could not apply filter on search results: Unknown where node: ' + str(type(where_node)))\n\n    def _get_filters_from_queryset(self):\n        return self._get_filters_from_where_node(self.queryset.query.where)\n\n\nclass BaseSearchResults(object):\n    def __init__(self, backend, query, prefetch_related=None):\n        self.backend = backend\n        self.query = query\n        self.prefetch_related = prefetch_related\n        self.start = 0\n        self.stop = None\n        self._results_cache = None\n        self._count_cache = None\n\n    def _set_limits(self, start=None, stop=None):\n        if stop is not None:\n            if self.stop is not None:\n                self.stop = min(self.stop, self.start + stop)\n            else:\n                self.stop = self.start + stop\n\n        if start is not None:\n            if self.stop is not None:\n                self.start = min(self.stop, self.start + start)\n            else:\n                self.start = self.start + start\n\n    def _clone(self):\n        klass = self.__class__\n        new = klass(self.backend, self.query, prefetch_related=self.prefetch_related)\n        new.start = self.start\n        new.stop = self.stop\n        return new\n\n    def _do_search(self):\n        raise NotImplementedError\n\n    def _do_count(self):\n        raise NotImplementedError\n\n    def results(self):\n        if self._results_cache is None:\n            self._results_cache = self._do_search()\n        return self._results_cache\n\n    def count(self):\n        if self._count_cache is None:\n            if self._results_cache is not None:\n                self._count_cache = len(self._results_cache)\n            else:\n                self._count_cache = self._do_count()\n        return self._count_cache\n\n    def __getitem__(self, key):\n        new = self._clone()\n\n        if isinstance(key, slice):\n            # Set limits\n            start = int(key.start) if key.start else None\n            stop = int(key.stop) if key.stop else None\n            new._set_limits(start, stop)\n\n            # Copy results cache\n            if self._results_cache is not None:\n                new._results_cache = self._results_cache[key]\n\n            return new\n        else:\n            if self._results_cache is not None:\n                return self._results_cache[key]\n\n            new.start = key\n            new.stop = key + 1\n            return list(new)[0]\n\n    def __iter__(self):\n        return iter(self.results())\n\n    def __len__(self):\n        return len(self.results())\n\n    def __repr__(self):\n        data = list(self[:21])\n        if len(data) > 20:\n            data[-1] = \"...(remaining elements truncated)...\"\n        return repr(data)\n\n\nclass BaseSearch(object):\n    def __init__(self, params):\n        pass\n\n    def reset_index(self):\n        raise NotImplementedError\n\n    def add_type(self, model):\n        raise NotImplementedError\n\n    def refresh_index(self):\n        raise NotImplementedError\n\n    def add(self, obj):\n        raise NotImplementedError\n\n    def add_bulk(self, model, obj_list):\n        raise NotImplementedError\n\n    def delete(self, obj):\n        raise NotImplementedError\n\n    def _search(self, queryset, query_string, fields=None):\n        raise NotImplementedError\n\n    def search(self, query_string, model_or_queryset, fields=None, filters=None, prefetch_related=None):\n        # Find model/queryset\n        if isinstance(model_or_queryset, QuerySet):\n            model = model_or_queryset.model\n            queryset = model_or_queryset\n        else:\n            model = model_or_queryset\n            queryset = model_or_queryset.objects.all()\n\n        # Model must be a class that is in the index\n        if not class_is_indexed(model):\n            return []\n\n        # Check that theres still a query string after the clean up\n        if query_string == \"\":\n            return []\n\n        # Apply filters to queryset\n        if filters:\n            queryset = queryset.filter(**filters)\n\n        # Prefetch related\n        if prefetch_related:\n            for prefetch in prefetch_related:\n                queryset = queryset.prefetch_related(prefetch)\n\n        # Search\n        return self._search(queryset, query_string, fields=fields)\n", "path": "wagtail/wagtailsearch/backends/base.py"}]}