Browse Source

Add support for specifying an operator on Fuzzy queries (#12714)

Tom Usher 3 months ago
parent
commit
75e07498f4

+ 1 - 0
CHANGELOG.txt

@@ -27,6 +27,7 @@ Changelog
  * Skip loading of unused JavaScript to speed up 404 page rendering (Sage Abdullah)
  * Replace l18n library with JavaScript Intl API for time zone options in Account view (Sage Abdullah)
  * Use explicit label for defaulting to server language in account settings (Sage Abdullah)
+ * Add support for specifying an operator on `Fuzzy` queries (Tom Usher)
  * Fix: Improve handling of translations for bulk page action confirmation messages (Matt Westcott)
  * Fix: Ensure custom rich text feature icons are correctly handled when provided as a list of SVG paths (Temidayo Azeez, Joel William, LB (Ben) Johnston)
  * Fix: Ensure manual edits to `StreamField` values do not throw an error (Stefan Hammer)

+ 1 - 0
docs/releases/6.4.md

@@ -31,6 +31,7 @@ depth: 1
  * Skip loading of unused JavaScript to speed up 404 page rendering (Sage Abdullah)
  * Replace l18n library with JavaScript Intl API for time zone options in Account view (Sage Abdullah)
  * Use explicit label for defaulting to server language in account settings (Sage Abdullah)
+ * Add support for specifying an operator on `Fuzzy` queries (Tom Usher)
 
 ### Bug fixes
 

+ 8 - 1
docs/topics/search/searching.md

@@ -197,11 +197,18 @@ For example:
 >>> from wagtail.search.query import Fuzzy
 
 >>> Page.objects.search(Fuzzy("Hallo"))
-[<Page: Hello World>]
+[<Page: Hello World>, <Page: Hello>]
 ```
 
 Fuzzy matching is supported by the Elasticsearch search backend only.
 
+The `operator` keyword argument is also supported on `Fuzzy` objects, defaulting to "or":
+
+```python
+>>> Page.objects.search(Fuzzy("Hallo wurld", operator="and"))
+[<Page: Hello World>]
+```
+
 (wagtailsearch_complex_queries)=
 
 ### Complex search queries

+ 4 - 0
wagtail/search/backends/elasticsearch7.py

@@ -605,6 +605,10 @@ class Elasticsearch7SearchQueryCompiler(BaseSearchQueryCompiler):
             "query": query.query_string,
             "fuzziness": "AUTO",
         }
+
+        if query.operator != "or":
+            match_query["operator"] = query.operator
+
         if len(fields) == 1:
             if fields[0].boost != 1.0:
                 match_query["boost"] = fields[0].boost

+ 8 - 2
wagtail/search/query.py

@@ -50,11 +50,17 @@ class Phrase(SearchQuery):
 
 
 class Fuzzy(SearchQuery):
-    def __init__(self, query_string: str):
+    OPERATORS = ["and", "or"]
+    DEFAULT_OPERATOR = "or"
+
+    def __init__(self, query_string: str, operator: str = DEFAULT_OPERATOR):
         self.query_string = query_string
+        self.operator = operator.lower()
+        if self.operator not in self.OPERATORS:
+            raise ValueError("`operator` must be either 'or' or 'and'.")
 
     def __repr__(self):
-        return f"<Fuzzy {repr(self.query_string)}>"
+        return f"<Fuzzy {repr(self.query_string)} operator={repr(self.operator)}>"
 
 
 class MatchAll(SearchQuery):

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

@@ -840,6 +840,27 @@ class TestElasticsearch7SearchQuery(TestCase):
         }
         self.assertDictEqual(query_compiler.get_inner_query(), expected_result)
 
+    def test_fuzzy_query_with_operator(self):
+        # Create a query
+        query_compiler = self.query_compiler_class(
+            models.Book.objects.all(),
+            Fuzzy("Hello world", operator="and"),
+        )
+
+        # Check it
+        expected_result = {
+            "multi_match": {
+                "fields": [
+                    "_all_text",
+                    "_all_text_boost_2_0^2.0",
+                ],
+                "query": "Hello world",
+                "fuzziness": "AUTO",
+                "operator": "and",
+            }
+        }
+        self.assertDictEqual(query_compiler.get_inner_query(), expected_result)
+
     def test_year_filter(self):
         # Create a query
         query_compiler = self.query_compiler_class(