diff --git a/AUTHORS b/AUTHORS index 70c18d8e18..13010b754d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -390,6 +390,7 @@ answer newbie questions, and generally made Django that much better: James Murty James Tauber James Timmins + James Turk James Wheare Jannis Leidel Janos Guljas diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py index 08d92e3514..c3b940d436 100644 --- a/django/contrib/postgres/search.py +++ b/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'): diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 58b16d2b48..fad87081b0 100644 --- a/django/db/backends/postgresql/features.py +++ b/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')) diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt index c78183cd01..813a3db57a 100644 --- a/docs/ref/contrib/postgres/search.txt +++ b/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`` ============== diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index b98d770ac5..136f2900b8 100644 --- a/docs/releases/3.1.txt +++ b/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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py index f5111ce8d3..07b1f9e9b4 100644 --- a/tests/postgres_tests/test_search.py +++ b/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')