Index: htdocs/js/query.js
===================================================================
--- htdocs/js/query.js	(revision 4789)
+++ htdocs/js/query.js	(working copy)
@@ -219,7 +219,7 @@
       td.className = "filter";
       if (property.type == "select") {
         var element = createSelect(propertyName, property.options, true);
-      } else if (property.type == "text") {
+      } else if (property.type == "text" || property.type == "keywords") {
         var element = document.createElement("input");
         element.type = "text";
         element.name = propertyName;
Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py	(revision 4789)
+++ trac/ticket/api.py	(working copy)
@@ -172,9 +172,8 @@
             fields.append(field)
 
         # Advanced text fields
-        for name in ('keywords', 'cc', ):
-            field = {'name': name, 'type': 'text', 'label': name.title()}
-            fields.append(field)
+        fields.append({'name': 'keywords', 'type': 'keywords', 'label': 'Keywords'})
+        fields.append({'name': 'cc', 'type': 'text', 'label': 'Cc'})
 
         for field in self.get_custom_fields():
             if field['name'] in [f['name'] for f in fields]:
Index: trac/ticket/tests/query.py
===================================================================
--- trac/ticket/tests/query.py	(revision 4789)
+++ trac/ticket/tests/query.py	(working copy)
@@ -312,6 +312,20 @@
         self.assertEqual([], args)
         tickets = query.execute(Mock(href=self.env.href))
 
+    def test_constrained_allof_keywords(self):
+        query = Query.from_string(self.env, None,
+                                  'keywords@=foo bar',
+                                  order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.keywords AS keywords,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE (COALESCE(t.keywords,'') LIKE %s ESCAPE '/' OR COALESCE(t.keywords,'') LIKE %s ESCAPE '/')
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['%foo%', '%bar%'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
     def test_csv_escape(self):
         query = Mock(get_columns=lambda: ['col1'],
                      execute=lambda r,c: [{'col1': 'value, needs escaped'}])
@@ -336,7 +350,6 @@
                          '<em class="error">[Error: Query filter requires '
                          'field and constraints separated by a "="]</em>')
 
-
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(QueryTestCase, 'test'))
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 4789)
+++ trac/ticket/query.py	(working copy)
@@ -91,7 +91,7 @@
                 raise QuerySyntaxError('Query filter requires field name')
             # from last char of `field`, get the mode of comparison
             mode, neg = '', ''
-            if field[-1] in ('~', '^', '$'):
+            if field[-1] in ('~', '^', '$', '@'):
                 mode = field[-1]
                 field = field[:-1]
             if field[-1] == '!':
@@ -138,7 +138,7 @@
                     if k != 'id' and k in cols]:
             constraint = self.constraints[col]
             if len(constraint) == 1 and constraint[0] \
-                    and not constraint[0][0] in ('!', '~', '^', '$'):
+                    and not constraint[0][0] in ('!', '~', '^', '$', '@'):
                 if col in cols:
                     cols.remove(col)
             if col == 'status' and not 'closed' in constraint \
@@ -301,11 +301,36 @@
 
             if mode == '':
                 return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''),
-                        value)
+                        [value])
             if not value:
                 return None
             db = self.env.get_db_cnx()
             value = db.like_escape(value)
+
+            # special case - search for keywords separated by space
+            if mode == '@':
+                words = value.split(' ')
+                con = "("
+                count = 0
+                args = []
+                
+                # iterate the words
+                for w in words:
+                    # word is empty, let's skip it
+                    if w == '':
+                        continue
+                    if count > 0:
+                        # need to append to the OR sequence
+                        con = con + " OR ";
+                    con = con + "COALESCE(%s,'') %s" % (name, db.like())
+                    args.append('%' + w + '%')
+                    count = count + 1
+
+                if (count == 0):
+                    # there are no words to filter by
+                    return None
+                con = con + ")"
+                return (con, args)
             if mode == '~':
                 value = '%' + value + '%'
             elif mode == '^':
@@ -314,7 +339,7 @@
                 value = '%' + value
             return ("COALESCE(%s,'') %s%s" % (name, neg and 'NOT ' or '',
                                               db.like()),
-                    value)
+                    [value])
 
         clauses = []
         args = []
@@ -323,7 +348,7 @@
             # starts-with, negation, etc.)
             neg = v[0].startswith('!')
             mode = ''
-            if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'):
+            if len(v[0]) > neg and v[0][neg] in ('~', '^', '$', '@'):
                 mode = v[0][neg]
 
             # Special case id ranges
@@ -368,12 +393,13 @@
                 else:
                     clauses.append("(" + " OR ".join(
                         [item[0] for item in constraint_sql]) + ")")
-                args += [item[1] for item in constraint_sql]
+                for item in constraint_sql:
+                    args.extend(item[1])
             elif len(v) == 1:
                 constraint_sql = get_constraint_sql(k, v[0], mode, neg)
                 if constraint_sql:
                     clauses.append(constraint_sql[0])
-                    args.append(constraint_sql[1])
+                    args.extend(constraint_sql[1])
 
         clauses = filter(None, clauses)
         if clauses:
@@ -438,7 +464,7 @@
                 if neg:
                     val = val[1:]
                 mode = ''
-                if val[:1] in ('~', '^', '$'):
+                if val[:1] in ('~', '^', '$', '@'):
                     mode, val = val[:1], val[1:]
                 constraint['mode'] = (neg and '!' or '') + mode
                 constraint['values'].append(val)
@@ -479,6 +505,8 @@
             {'name': "is", 'value': ""},
             {'name': "is not", 'value': "!"}
         ]
+        modes['keywords'] = [{'name': "contains all of", 'value': "@"}]
+        modes['keywords'].extend(modes['text'])
 
         groups = {}
         groupsequence = []
