diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 305c2b55fc1cdc103ee3a6fb9ea9d06a518aa148..8962b88ba70bf6cdf471a1cbaa5e56433324679f 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -2702,14 +2702,58 @@ class ProjectTags(models.Model):
 
     @api.model
     def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
-        domain = args
-        if 'project_id' in self.env.context:
-            domain = self._get_project_tags_domain(domain, self.env.context.get('project_id'))
-        return super()._name_search(name, domain, operator, limit, name_get_uid)
-
-    @api.model
-    def name_create(self, name):
-        existing_tag = self.search([('name', '=ilike', name.strip())], limit=1)
-        if existing_tag:
-            return existing_tag.name_get()[0]
-        return super().name_create(name)
+        if 'project_id' in self.env.context and operator == 'ilike':
+            # `args` has the form of the default filter ['!', ['id', 'in', <ids>]]
+            # passed to exclude already selected tags -> exclude them in our query too
+            excluded_ids = list(args[1][2]) \
+                if args and len(args) == 2 and args[0] == '!' and len(args[1]) == 3 and args[1][:2] == ["id", "in"] \
+                else []
+            # UNION ALL is lazy evaluated, if the first query has enough results,
+            # the second is not executed (just planned).
+            query = """
+                WITH query_tags_in_tasks AS (
+                    SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 1 AS sequence
+                    FROM project_tags AS tags
+                    JOIN (
+                        SELECT project_tags_id
+                        FROM project_tags_project_task_rel AS rel
+                        JOIN project_task AS task
+                            ON task.project_id = %(project_id)s
+                            AND task.id = rel.project_task_id
+                        ORDER BY task.id DESC
+                        LIMIT 1000 -- arbitrary limit to speed up lookup on huge projects (fallback below on global scope)
+                    ) AS tags__tasks_ids
+                        ON tags__tasks_ids.project_tags_id = tags.id
+                    WHERE tags.id != ALL(%(excluded_ids)s)
+                    AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s
+                    GROUP BY 1, 2, 3  -- faster than a distinct
+                    LIMIT %(limit)s
+                ), query_all_tags AS (
+                    SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 2 AS sequence
+                    FROM project_tags AS tags
+                    WHERE tags.id != ALL(%(excluded_ids)s)
+                    AND tags.id NOT IN (SELECT id FROM query_tags_in_tasks)
+                    AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s
+                    LIMIT %(limit)s
+                )
+                SELECT id FROM (
+                    SELECT id, name, sequence
+                    FROM query_tags_in_tasks
+                    UNION ALL
+                    SELECT id, name, sequence
+                    FROM query_all_tags
+                    LIMIT %(limit)s
+                ) AS tags
+                ORDER BY sequence, name
+            """
+            params = {
+                'project_id': self.env.context.get('project_id'),
+                'excluded_ids': excluded_ids,
+                'limit': limit,
+                'lang': self.env.context.get('lang', 'en_US'),
+                'search_term': '%' + name + '%',
+            }
+            self.env.cr.execute(query, params)
+            return [row[0] for row in self.env.cr.fetchall()]
+        else:
+            return super()._name_search(name, args, operator, limit, name_get_uid)