Quellcode durchsuchen

Remove use of partial_match on search

Matt Westcott vor 1 Jahr
Ursprung
Commit
db691b5f1b

+ 8 - 4
docs/topics/search/indexing.md

@@ -91,7 +91,6 @@ These are used for performing full-text searches on your models, usually for tex
 
 #### Options
 
--   **partial_match** (`boolean`) - Setting this to true allows results to be matched on parts of words. For example, this is set on the title field by default, so a page titled `Hello World!` will be found if the user only types `Hel` into the search box.
 -   **boost** (`int/float`) - This allows you to set fields as being more important than others. Setting this to a high number on a field will cause pages with matches in that field to be ranked higher. By default, this is set to 2 on the Page title field and 1 on all other fields.
 
     ```{note}
@@ -107,18 +106,22 @@ These are used for performing full-text searches on your models, usually for tex
 
 -   **es_extra** (`dict`) - This field is to allow the developer to set or override any setting on the field in the Elasticsearch mapping. Use this if you want to make use of any Elasticsearch features that are not yet supported in Wagtail.
 
-(wagtailsearch_index_filterfield)=
+```{versionchanged} 5.0
+The `partial_match` option has been deprecated. To index a field for partial matching, use `AutocompleteField` instead.
+```
 
 ### `index.AutocompleteField`
 
 These are used for autocomplete queries that match partial words. For example, a page titled `Hello World!` will be found if the user only types `Hel` into the search box.
 
-This takes the exact same options as `index.SearchField` (with the exception of `partial_match`, which has no effect).
+This takes the same options as `index.SearchField`.
 
 ```{note}
 `index.AutocompleteField` should only be used on fields that are displayed in the search results. This allows users to see any words that were partial-matched.
 ```
 
+(wagtailsearch_index_filterfield)=
+
 ### `index.FilterField`
 
 These are added to the search index but are not used for full-text searches. Instead, they allow you to run filters on your search results.
@@ -236,7 +239,8 @@ class Book(index.Indexed, models.Model):
     published_date = models.DateTimeField()
 
     search_fields = [
-        index.SearchField('title', partial_match=True, boost=10),
+        index.SearchField('title', boost=10),
+        index.AutocompleteField('title', boost=10),
         index.SearchField('get_genre_display'),
 
         index.FilterField('genre'),

+ 5 - 2
docs/topics/search/searching.md

@@ -30,8 +30,11 @@ All other methods of `PageQuerySet` can be used with `search()`. For example:
 The `search()` method will convert your `QuerySet` into an instance of one of Wagtail's `SearchResults` classes (depending on backend). This means that you must perform filtering before calling `search()`.
 ```
 
-Before the `autocomplete()` method was introduced, the search method also did partial matching. This behaviour is deprecated and you should either switch to the new `autocomplete()` method or pass `partial_match=False` into the search method to opt-in to the new behaviour.
-The partial matching in `search()` will be completely removed in a future release.
+The standard behaviour of the `search` method is to only return matches for complete words; for example, a search for "hel" will not return results containing the word "hello". The exception to this is the fallback database search backend, used when the database does not have full-text search extensions available, and no alternative backend has been specified. This performs basic substring matching, and will return results containing the search term ignoring all word boundaries.
+
+```{versionchanged} 5.0
+The Elasticsearch backend now defaults to matching on complete words. Previously, this backend performed partial matching by default, and it was necessary to pass the keyword argument `partial_match=False` to disable this. To perform searches with partial matching behaviour, you should instead use the `autocomplete` method (see below) in conjunction with `AutocompleteField`. Any occurrences of `partial_match=False` in your code can now be removed.
+```
 
 ### Autocomplete searches
 

+ 2 - 1
docs/topics/snippets.md

@@ -266,7 +266,8 @@ class Advert(index.Indexed, models.Model):
     ]
 
     search_fields = [
-        index.SearchField('text', partial_match=True),
+        index.SearchField('text'),
+        index.AutocompleteField('text'),
     ]
 ```
 

+ 1 - 18
wagtail/admin/forms/choosers.py

@@ -76,24 +76,7 @@ class SearchFilterMixin(forms.Form):
         search_query = self.cleaned_data.get("q")
         if search_query:
             search_backend = get_search_backend()
-
-            # The search should work as an autocomplete by preference, but only if
-            # there are AutocompleteFields set up for this model in the search index;
-            # if not, fall back on a standard search so that we get results at all
-            if objects.model.get_autocomplete_search_fields():
-                try:
-                    objects = search_backend.autocomplete(search_query, objects)
-                except NotImplementedError:
-                    # Older search backends do not implement .autocomplete() but do support
-                    # partial_match on .search(). Newer ones will ignore partial_match.
-                    objects = search_backend.search(
-                        search_query, objects, partial_match=True
-                    )
-            else:
-                objects = search_backend.search(
-                    search_query, objects, partial_match=True
-                )
-
+            objects = search_backend.autocomplete(search_query, objects)
             self.is_searching = True
             self.search_query = search_query
         return objects

+ 0 - 2
wagtail/admin/tests/test_page_chooser.py

@@ -330,8 +330,6 @@ class TestChooserSearch(WagtailTestUtils, TransactionTestCase):
         self.assertContains(response, "foobarbaz")
 
     def test_partial_match(self):
-        # FIXME: SQLite and MySQL FTS backends don't support autocomplete searches
-        # https://github.com/wagtail/wagtail/issues/9903
         response = self.get({"q": "fooba"})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailadmin/chooser/_search_results.html")

+ 1 - 6
wagtail/admin/views/chooser.py

@@ -451,12 +451,7 @@ class SearchView(View):
             pages = pages.exclude(depth=1)  # never include root
             pages = pages.type(*desired_classes)
             pages = pages.specific()
-            try:
-                pages = pages.autocomplete(search_form.cleaned_data["q"])
-            except NotImplementedError:
-                # Older search backends do not implement .autocomplete() but do support
-                # partial_match on .search(). Newer ones will ignore partial_match.
-                pages = pages.search(search_form.cleaned_data["q"], partial_match=True)
+            pages = pages.autocomplete(search_form.cleaned_data["q"])
 
         else:
             pages = pages.none()

+ 0 - 2
wagtail/contrib/modeladmin/helpers/search.py

@@ -66,7 +66,6 @@ class WagtailBackendSearchHandler(BaseSearchHandler):
         search_term,
         preserve_order=False,
         operator=None,
-        partial_match=True,
         backend=None,
         **kwargs,
     ):
@@ -79,6 +78,5 @@ class WagtailBackendSearchHandler(BaseSearchHandler):
             queryset,
             fields=self.search_fields or None,
             operator=operator,
-            partial_match=partial_match,
             order_by_relevance=not preserve_order,
         )

+ 0 - 5
wagtail/contrib/modeladmin/tests/test_search_handlers.py

@@ -20,7 +20,6 @@ class FakeSearchBackend:
         fields=None,
         operator=None,
         order_by_relevance=True,
-        partial_match=True,
     ):
         return {
             "query": query,
@@ -28,7 +27,6 @@ class FakeSearchBackend:
             "fields": fields,
             "operator": operator,
             "order_by_relevance": order_by_relevance,
-            "partial_match": partial_match,
         }
 
 
@@ -110,7 +108,6 @@ class TestSearchBackendHandler(TestCase):
                 "fields": None,
                 "operator": None,
                 "order_by_relevance": True,
-                "partial_match": True,
             },
         )
 
@@ -134,7 +131,6 @@ class TestSearchBackendHandler(TestCase):
                 "fields": search_fields,
                 "operator": None,
                 "order_by_relevance": True,
-                "partial_match": True,
             },
         )
 
@@ -157,7 +153,6 @@ class TestSearchBackendHandler(TestCase):
                 "fields": None,
                 "operator": None,
                 "order_by_relevance": False,
-                "partial_match": True,
             },
         )
 

+ 2 - 2
wagtail/documents/models.py

@@ -44,13 +44,13 @@ class AbstractDocument(CollectionMember, index.Indexed, models.Model):
     objects = DocumentQuerySet.as_manager()
 
     search_fields = CollectionMember.search_fields + [
-        index.SearchField("title", partial_match=True, boost=10),
+        index.SearchField("title", boost=10),
         index.AutocompleteField("title"),
         index.FilterField("title"),
         index.RelatedFields(
             "tags",
             [
-                index.SearchField("name", partial_match=True, boost=10),
+                index.SearchField("name", boost=10),
                 index.AutocompleteField("name"),
             ],
         ),

+ 1 - 13
wagtail/documents/views/documents.py

@@ -67,19 +67,7 @@ class BaseListingView(TemplateView):
                 query_string = self.form.cleaned_data["q"]
                 if query_string:
                     search_backend = get_search_backend()
-                    if documents.model.get_autocomplete_search_fields():
-                        try:
-                            documents = search_backend.autocomplete(
-                                query_string, documents
-                            )
-                        except NotImplementedError:
-                            documents = search_backend.search(
-                                query_string, documents, partial_match=True
-                            )
-                    else:
-                        documents = search_backend.search(
-                            query_string, documents, partial_match=True
-                        )
+                    documents = search_backend.autocomplete(query_string, documents)
         else:
             self.form = SearchForm(placeholder=_("Search documents"))
 

+ 2 - 2
wagtail/images/models.py

@@ -318,13 +318,13 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
         return reverse("wagtailimages:image_usage", args=(self.id,))
 
     search_fields = CollectionMember.search_fields + [
-        index.SearchField("title", partial_match=True, boost=10),
+        index.SearchField("title", boost=10),
         index.AutocompleteField("title"),
         index.FilterField("title"),
         index.RelatedFields(
             "tags",
             [
-                index.SearchField("name", partial_match=True, boost=10),
+                index.SearchField("name", boost=10),
                 index.AutocompleteField("name"),
             ],
         ),

+ 1 - 11
wagtail/images/views/images.py

@@ -107,17 +107,7 @@ class BaseListingView(TemplateView):
                 query_string = self.form.cleaned_data["q"]
                 if query_string:
                     search_backend = get_search_backend()
-                    if images.model.get_autocomplete_search_fields():
-                        try:
-                            images = search_backend.autocomplete(query_string, images)
-                        except NotImplementedError:
-                            images = search_backend.search(
-                                query_string, images, partial_match=True
-                            )
-                    else:
-                        images = search_backend.search(
-                            query_string, images, partial_match=True
-                        )
+                    images = search_backend.autocomplete(query_string, images)
         else:
             self.form = SearchForm(placeholder=_("Search images"))
 

+ 1 - 1
wagtail/models/__init__.py

@@ -1158,7 +1158,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
     alias_of.wagtail_reference_index_ignore = True
 
     search_fields = [
-        index.SearchField("title", partial_match=True, boost=2),
+        index.SearchField("title", boost=2),
         index.AutocompleteField("title"),
         index.FilterField("title"),
         index.FilterField("id"),

+ 4 - 4
wagtail/search/backends/base.py

@@ -42,7 +42,7 @@ class BaseSearchQueryCompiler:
         fields=None,
         operator=None,
         order_by_relevance=True,
-        partial_match=True,
+        partial_match=None,  # RemovedInWagtail60Warning
     ):
         self.queryset = queryset
         if query is None:
@@ -56,7 +56,7 @@ class BaseSearchQueryCompiler:
         self.query = query
         self.fields = fields
         self.order_by_relevance = order_by_relevance
-        self.partial_match = partial_match
+        self.partial_match = partial_match  # RemovedInWagtail60Warning
 
     def _get_filterable_field(self, field_attname):
         # Get field
@@ -427,7 +427,7 @@ class BaseSearchBackend:
         fields=None,
         operator=None,
         order_by_relevance=True,
-        partial_match=True,
+        partial_match=None,  # RemovedInWagtail60Warning
     ):
         return self._search(
             self.query_compiler_class,
@@ -436,7 +436,7 @@ class BaseSearchBackend:
             fields=fields,
             operator=operator,
             order_by_relevance=order_by_relevance,
-            partial_match=partial_match,
+            partial_match=partial_match,  # RemovedInWagtail60Warning
         )
 
     def autocomplete(

+ 8 - 1
wagtail/search/index.py

@@ -1,5 +1,6 @@
 import inspect
 import logging
+from warnings import warn
 
 from django.apps import apps
 from django.core import checks
@@ -9,6 +10,7 @@ from django.db.models.fields.related import ForeignObjectRel, OneToOneRel, Relat
 from modelcluster.fields import ParentalManyToManyField
 
 from wagtail.search.backends import get_search_backends_with_name
+from wagtail.utils.deprecation import RemovedInWagtail60Warning
 
 logger = logging.getLogger("wagtail.search.index")
 
@@ -295,8 +297,13 @@ class BaseField:
 class SearchField(BaseField):
     def __init__(self, field_name, boost=None, partial_match=False, **kwargs):
         super().__init__(field_name, **kwargs)
+        if partial_match:
+            warn(
+                "The partial_match option on SearchField has no effect and will be removed. "
+                "Use AutocompleteField instead",
+                category=RemovedInWagtail60Warning,
+            )
         self.boost = boost
-        self.partial_match = partial_match
 
 
 class AutocompleteField(BaseField):

+ 2 - 2
wagtail/search/queryset.py

@@ -8,7 +8,7 @@ class SearchableQuerySetMixin:
         fields=None,
         operator=None,
         order_by_relevance=True,
-        partial_match=True,
+        partial_match=None,  # RemovedInWagtail60Warning
         backend="default",
     ):
         """
@@ -21,7 +21,7 @@ class SearchableQuerySetMixin:
             fields=fields,
             operator=operator,
             order_by_relevance=order_by_relevance,
-            partial_match=partial_match,
+            partial_match=partial_match,  # RemovedInWagtail60Warning
         )
 
     def autocomplete(

+ 2 - 2
wagtail/search/tests/elasticsearch_common_tests.py

@@ -41,14 +41,14 @@ class ElasticsearchCommonSearchBackendTests(BackendTests):
         )
 
     def test_disabled_partial_search(self):
-        results = self.backend.search("Java", models.Book, partial_match=False)
+        results = self.backend.search("Java", models.Book)
 
         self.assertUnsortedListEqual([r.title for r in results], [])
 
     def test_disabled_partial_search_with_whole_term(self):
         # Making sure that there isn't a different reason why the above test
         # returned no results
-        results = self.backend.search("JavaScript", models.Book, partial_match=False)
+        results = self.backend.search("JavaScript", models.Book)
 
         self.assertUnsortedListEqual(
             [r.title for r in results],

+ 3 - 0
wagtail/search/tests/test_backends.py

@@ -249,6 +249,9 @@ class BackendTests(WagtailTestUtils):
     def test_autocomplete_uses_autocompletefield(self):
         # Autocomplete should only require an AutocompleteField, not a SearchField with
         # partial_match=True
+        # TODO: given that partial_match=True has no effect as of Wagtail 5, also test that
+        # AutocompleteField is actually being respected, and it's not just relying on the
+        # presence of a SearchField (with or without partial_match)
         results = self.backend.autocomplete("Georg", models.Author)
         self.assertUnsortedListEqual(
             [r.name for r in results],

+ 0 - 3
wagtail/search/tests/test_elasticsearch5_backend.py

@@ -570,7 +570,6 @@ class TestElasticsearch5SearchQuery(TestCase):
         query_compiler = self.query_compiler_class(
             models.Book.objects.all(),
             Fuzzy("Hello world"),
-            partial_match=False,
         )
 
         # Check it
@@ -585,7 +584,6 @@ class TestElasticsearch5SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title"],
-            partial_match=False,
         )
 
         # Check it
@@ -600,7 +598,6 @@ class TestElasticsearch5SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title", "body"],
-            partial_match=False,
         )
 
         # Check it

+ 0 - 3
wagtail/search/tests/test_elasticsearch6_backend.py

@@ -666,7 +666,6 @@ class TestElasticsearch6SearchQuery(TestCase):
         query_compiler = self.query_compiler_class(
             models.Book.objects.all(),
             Fuzzy("Hello world"),
-            partial_match=False,
         )
 
         # Check it
@@ -681,7 +680,6 @@ class TestElasticsearch6SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title"],
-            partial_match=False,
         )
 
         # Check it
@@ -696,7 +694,6 @@ class TestElasticsearch6SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title", "body"],
-            partial_match=False,
         )
 
         # Check it

+ 0 - 3
wagtail/search/tests/test_elasticsearch7_backend.py

@@ -652,7 +652,6 @@ class TestElasticsearch7SearchQuery(TestCase):
         query_compiler = self.query_compiler_class(
             models.Book.objects.all(),
             Fuzzy("Hello world"),
-            partial_match=False,
         )
 
         # Check it
@@ -667,7 +666,6 @@ class TestElasticsearch7SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title"],
-            partial_match=False,
         )
 
         # Check it
@@ -682,7 +680,6 @@ class TestElasticsearch7SearchQuery(TestCase):
             models.Book.objects.all(),
             Fuzzy("Hello world"),
             fields=["title", "body"],
-            partial_match=False,
         )
 
         # Check it

+ 4 - 7
wagtail/search/tests/test_indexed_class.py

@@ -51,7 +51,7 @@ class TestSearchFields(TestCase):
     def test_basic(self):
         cls = self.make_dummy_type(
             [
-                index.SearchField("test", boost=100, partial_match=False),
+                index.SearchField("test", boost=100),
                 index.FilterField("filter_test"),
             ]
         )
@@ -72,8 +72,8 @@ class TestSearchFields(TestCase):
         # as intended.
         cls = self.make_dummy_type(
             [
-                index.SearchField("test", boost=100, partial_match=False),
-                index.SearchField("test", partial_match=True),
+                index.SearchField("test", boost=100),
+                index.SearchField("test"),
             ]
         )
 
@@ -87,14 +87,11 @@ class TestSearchFields(TestCase):
         # Boost should be reset to the default if it's not specified by the override
         self.assertIsNone(field.boost)
 
-        # Check that the partial match was overridden
-        self.assertTrue(field.partial_match)
-
     def test_different_field_types_dont_override(self):
         # A search and filter field with the same name should be able to coexist
         cls = self.make_dummy_type(
             [
-                index.SearchField("test", boost=100, partial_match=False),
+                index.SearchField("test", boost=100),
                 index.FilterField("test"),
             ]
         )