Fixed #31340 -- Allowed query expressions in SearchQuery.value and __search lookup.
diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py
index 90b6823575423..2b2ae0c321b2d 100644
--- a/django/contrib/postgres/search.py
+++ b/django/contrib/postgres/search.py
@@ -11,7 +11,7 @@ class SearchVectorExact(Lookup):
     lookup_name = 'exact'
 
     def process_rhs(self, qn, connection):
-        if not hasattr(self.rhs, 'resolve_expression'):
+        if not isinstance(self.rhs, (SearchQuery, CombinedSearchQuery)):
             config = getattr(self.lhs, 'config', None)
             self.rhs = SearchQuery(self.rhs, config=config)
         rhs, rhs_params = super().process_rhs(qn, connection)
@@ -170,7 +170,8 @@ def __init__(self, value, output_field=None, *, config=None, invert=False, searc
         self.function = self.SEARCH_TYPES.get(search_type)
         if self.function is None:
             raise ValueError("Unknown search_type argument '%s'." % search_type)
-        value = Value(value)
+        if not hasattr(value, 'resolve_expression'):
+            value = Value(value)
         expressions = (value,)
         self.config = SearchConfig.from_parameter(config)
         if self.config is not None:
diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt
index 949d95929e558..65d54cfd8dd18 100644
--- a/docs/ref/contrib/postgres/search.txt
+++ b/docs/ref/contrib/postgres/search.txt
@@ -35,6 +35,10 @@ query and the vector.
 To use the ``search`` lookup, ``'django.contrib.postgres'`` must be in your
 :setting:`INSTALLED_APPS`.
 
+.. versionchanged:: 3.1
+
+    Support for query expressions was added.
+
 ``SearchVector``
 ================
 
@@ -108,7 +112,8 @@ See :ref:`postgresql-fts-search-configuration` for an explanation of the
 
 .. versionchanged:: 3.1
 
-    Support for ``'websearch'`` search type was added.
+    Support for ``'websearch'`` search type and query expressions in
+    ``SearchQuery.value`` were added.
 
 ``SearchRank``
 ==============
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 278752db90c5f..de66d7805f72d 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -113,9 +113,14 @@ Minor features
 * :class:`~django.contrib.postgres.search.SearchQuery` now supports
   ``'websearch'`` search type on PostgreSQL 11+.
 
+* :class:`SearchQuery.value <django.contrib.postgres.search.SearchQuery>` now
+  supports query expressions.
+
 * The new :class:`~django.contrib.postgres.search.SearchHeadline` class allows
   highlighting search results.
 
+* :lookup:`search` lookup now supports query expressions.
+
 :mod:`django.contrib.redirects`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index 12d94e348a21d..ee1463e1eb7f4 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -185,6 +185,17 @@ class Migration(migrations.Migration):
             },
             bases=None,
         ),
+        migrations.CreateModel(
+            name='LineSavedSearch',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('line', models.ForeignKey('postgres_tests.Line', on_delete=models.CASCADE)),
+                ('query', models.CharField(max_length=100)),
+            ],
+            options={
+                'required_db_vendor': 'postgresql',
+            },
+        ),
         migrations.CreateModel(
             name='AggregateTestModel',
             fields=[
diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py
index 8528c59da1c06..e803c989e07a0 100644
--- a/tests/postgres_tests/models.py
+++ b/tests/postgres_tests/models.py
@@ -139,6 +139,11 @@ def __str__(self):
         return self.dialogue or ''
 
 
+class LineSavedSearch(PostgreSQLModel):
+    line = models.ForeignKey('Line', models.CASCADE)
+    query = models.CharField(max_length=100)
+
+
 class RangesModel(PostgreSQLModel):
     ints = IntegerRangeField(blank=True, null=True)
     bigints = BigIntegerRangeField(blank=True, null=True)
diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py
index 0e836e896b726..b40d6729201d8 100644
--- a/tests/postgres_tests/test_search.py
+++ b/tests/postgres_tests/test_search.py
@@ -10,7 +10,7 @@
 from django.test import modify_settings, skipUnlessDBFeature
 
 from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
-from .models import Character, Line, Scene
+from .models import Character, Line, LineSavedSearch, Scene
 
 try:
     from django.contrib.postgres.search import (
@@ -110,6 +110,18 @@ def test_search_query_config(self):
         )
         self.assertSequenceEqual(searched, [self.verse2])
 
+    def test_search_with_F_expression(self):
+        # Non-matching query.
+        LineSavedSearch.objects.create(line=self.verse1, query='hearts')
+        # Matching query.
+        match = LineSavedSearch.objects.create(line=self.verse1, query='elbows')
+        for query_expression in [F('query'), SearchQuery(F('query'))]:
+            with self.subTest(query_expression):
+                searched = LineSavedSearch.objects.filter(
+                    line__dialogue__search=query_expression,
+                )
+                self.assertSequenceEqual(searched, [match])
+
 
 @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
 class SearchVectorFieldTest(GrailTestData, PostgreSQLTestCase):

ticket-31340