Browse Source

Fixed #31088 -- Added support for websearch searching in SearchQuery.

James Turk 5 years ago
parent
commit
ff00a05347

+ 1 - 0
AUTHORS

@@ -390,6 +390,7 @@ answer newbie questions, and generally made Django that much better:
     James Murty
     James Tauber <jtauber@jtauber.com>
     James Timmins <jameshtimmins@gmail.com>
+    James Turk <dev@jamesturk.net>
     James Wheare <django@sparemint.com>
     Jannis Leidel <jannis@leidel.info>
     Janos Guljas

+ 1 - 0
django/contrib/postgres/search.py

@@ -134,6 +134,7 @@ class SearchQuery(SearchQueryCombinable, Value):
         'plain': 'plainto_tsquery',
         'phrase': 'phraseto_tsquery',
         'raw': 'to_tsquery',
+        'websearch': 'websearch_to_tsquery',
     }
 
     def __init__(self, value, output_field=None, *, config=None, invert=False, search_type='plain'):

+ 5 - 0
django/db/backends/postgresql/features.py

@@ -64,6 +64,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     def is_postgresql_10(self):
         return self.connection.pg_version >= 100000
 
+    @cached_property
+    def is_postgresql_11(self):
+        return self.connection.pg_version >= 110000
+
     @cached_property
     def is_postgresql_12(self):
         return self.connection.pg_version >= 120000
@@ -71,4 +75,5 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     has_bloom_index = property(operator.attrgetter('is_postgresql_9_6'))
     has_brin_autosummarize = property(operator.attrgetter('is_postgresql_10'))
     has_phraseto_tsquery = property(operator.attrgetter('is_postgresql_9_6'))
+    has_websearch_to_tsquery = property(operator.attrgetter('is_postgresql_11'))
     supports_table_partitions = property(operator.attrgetter('is_postgresql_10'))

+ 10 - 2
docs/ref/contrib/postgres/search.txt

@@ -80,8 +80,11 @@ looks for matches for all of the resulting terms.
 If ``search_type`` is ``'plain'``, which is the default, the terms are treated
 as separate keywords. If ``search_type`` is ``'phrase'``, the terms are treated
 as a single phrase. If ``search_type`` is ``'raw'``, then you can provide a
-formatted search query with terms and operators. Read PostgreSQL's `Full Text
-Search docs`_ to learn about differences and syntax. Examples:
+formatted search query with terms and operators. If ``search_type`` is
+``'websearch'``, then you can provide a formatted search query, similar to the
+one used by web search engines. ``'websearch'`` requires PostgreSQL ≥ 11. Read
+PostgreSQL's `Full Text Search docs`_ to learn about differences and syntax.
+Examples:
 
 .. _Full Text Search docs: https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
 
@@ -91,6 +94,7 @@ Search docs`_ to learn about differences and syntax. Examples:
     >>> SearchQuery('red tomato', search_type='phrase')  # a phrase
     >>> SearchQuery('tomato red', search_type='phrase')  # a different phrase
     >>> SearchQuery("'tomato' & ('red' | 'green')", search_type='raw')  # boolean operators
+    >>> SearchQuery("'tomato' ('red' OR 'green')", search_type='websearch')  # websearch operators
 
 ``SearchQuery`` terms can be combined logically to provide more flexibility::
 
@@ -102,6 +106,10 @@ Search docs`_ to learn about differences and syntax. Examples:
 See :ref:`postgresql-fts-search-configuration` for an explanation of the
 ``config`` parameter.
 
+.. versionchanged:: 3.1
+
+    Support for ``'websearch'`` search type was added.
+
 ``SearchRank``
 ==============
 

+ 3 - 0
docs/releases/3.1.txt

@@ -97,6 +97,9 @@ Minor features
   :class:`~django.db.models.SmallIntegerField`, and
   :class:`~django.db.models.DecimalField`.
 
+* :class:`~django.contrib.postgres.search.SearchQuery` now supports
+  ``'websearch'`` search type on PostgreSQL 11+.
+
 :mod:`django.contrib.redirects`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 39 - 0
tests/postgres_tests/test_search.py

@@ -202,6 +202,45 @@ class MultipleFieldsTest(GrailTestData, PostgreSQLTestCase):
         )
         self.assertSequenceEqual(searched, [self.french])
 
+    @skipUnlessDBFeature('has_websearch_to_tsquery')
+    def test_web_search(self):
+        line_qs = Line.objects.annotate(search=SearchVector('dialogue'))
+        searched = line_qs.filter(
+            search=SearchQuery(
+                '"burned body" "split kneecaps"',
+                search_type='websearch',
+            ),
+        )
+        self.assertSequenceEqual(searched, [])
+        searched = line_qs.filter(
+            search=SearchQuery(
+                '"body burned" "kneecaps split" -"nostrils"',
+                search_type='websearch',
+            ),
+        )
+        self.assertSequenceEqual(searched, [self.verse1])
+        searched = line_qs.filter(
+            search=SearchQuery(
+                '"Sir Robin" ("kneecaps" OR "Camelot")',
+                search_type='websearch',
+            ),
+        )
+        self.assertSequenceEqual(searched, [self.verse0, self.verse1])
+
+    @skipUnlessDBFeature('has_websearch_to_tsquery')
+    def test_web_search_with_config(self):
+        line_qs = Line.objects.annotate(
+            search=SearchVector('scene__setting', 'dialogue', config='french'),
+        )
+        searched = line_qs.filter(
+            search=SearchQuery('cadeau -beau', search_type='websearch', config='french'),
+        )
+        self.assertSequenceEqual(searched, [])
+        searched = line_qs.filter(
+            search=SearchQuery('beau cadeau', search_type='websearch', config='french'),
+        )
+        self.assertSequenceEqual(searched, [self.french])
+
     def test_bad_search_type(self):
         with self.assertRaisesMessage(ValueError, "Unknown search_type argument 'foo'."):
             SearchQuery('kneecaps', search_type='foo')