diff --git a/include/class.orm.php b/include/class.orm.php
index 72684b79991dfabeb95678d1c0beb85708ae980f..36f666b5c50ced84087b40a4949c0f6834ee3330 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -673,7 +673,7 @@ class SqlFunction {
         foreach ($this->args as $A) {
             $args[] = $this->input($A, $compiler, $model);
         }
-        return sprintf('%s(%s)%s', $this->func, implode(',', $args),
+        return sprintf('%s(%s)%s', $this->func, implode(', ', $args),
             $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
     }
 
@@ -826,7 +826,7 @@ class SqlCode extends SqlFunction {
     }
 
     function toSql($compiler, $model=false, $alias=false) {
-        return $this->code;
+        return $this->code.($alias ? ' AS '.$alias : '');
     }
 }
 
@@ -860,7 +860,7 @@ class SqlAggregate extends SqlFunction {
         // specification.
         $E = $this->expr;
         if ($E instanceof SqlFunction) {
-            $field = $E->toSql($compiler, $model, $alias);
+            $field = $E->toSql($compiler, $model);
         }
         else {
         list($field, $rmodel) = $compiler->getField($E, $model, $options);
@@ -1175,9 +1175,17 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         return $this;
     }
 
+    function countSelectFields() {
+        $count = count($this->values) + count($this->annotations);
+        if (isset($this->extra['select']))
+            foreach (@$this->extra['select'] as $S)
+                $count += count($S);
+        return $count;
+    }
+
     function union(QuerySet $other, $all=true) {
         // Values and values_list _must_ match for this to work
-        if (count($this->values) != count($other->values))
+        if ($this->countSelectFields() != $other->countSelectFields())
             throw new OrmException('Union queries must have matching values counts');
 
         // TODO: Clear OFFSET and LIMIT in the $other query
@@ -1242,7 +1250,9 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         // Load defaults from model
         $model = $this->model;
         $query = clone $this;
-        if (!$options['nosort'] && !$query->ordering && $model::getMeta('ordering'))
+        if ($options['nosort'])
+            $query->ordering = array();
+        elseif (!$query->ordering && $model::getMeta('ordering'))
             $query->ordering = $model::getMeta('ordering');
         if (false !== $query->related && !$query->values && $model::getMeta('select_related'))
             $query->related = $model::getMeta('select_related');
@@ -2219,7 +2229,7 @@ class MySqlCompiler extends SqlCompiler {
         // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
         // the query as a JOIN and add the join constraint into the WHERE
         // clause.
-        elseif ($b instanceof QuerySet && $b->isWindowed()) {
+        elseif ($b instanceof QuerySet && ($b->isWindowed() || $b->countSelectFields() > 1)) {
             $f1 = $b->values[0];
             $view = $b->asView();
             $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array()));
@@ -2371,14 +2381,15 @@ class MySqlCompiler extends SqlCompiler {
     }
 
     function compileCount($queryset) {
-        $model = $queryset->model;
-        $table = $model::getMeta('table');
-        list($where, $having) = $this->getWhereHavingClause($queryset);
-        $joins = $this->getJoins($queryset);
-        $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where;
-        $exec = new MysqlExecutor($sql, $this->params);
-        $row = $exec->getArray();
-        return $row['count'];
+        $q = clone $queryset;
+        // Drop extra fields from the queryset
+        $q->related = $q->anotations = false;
+        $model = $q->model;
+        $q->values = $model::getMeta('pk');
+        $exec = $q->getQuery(array('nosort' => true));
+        $exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
+        $row = $exec->getRow();
+        return $row ? $row[0] : null;
     }
 
     function compileSelect($queryset) {
@@ -2530,6 +2541,8 @@ class MySqlCompiler extends SqlCompiler {
             foreach ($queryset->extra['select'] as $name=>$expr) {
                 if ($expr instanceof SqlFunction)
                     $expr = $expr->toSql($this, false, $name);
+                else
+                    $expr = sprintf('%s AS %s', $expr, $this->quote($name));
                 $fields[] = $expr;
             }
         }
@@ -2555,6 +2568,9 @@ class MySqlCompiler extends SqlCompiler {
                     $self->params[] = $q->params[$m[1]-1];
                     return ':'.count($self->params);
                 }, $q->sql);
+                // Wrap unions in parentheses if they are windowed or sorted
+                if ($qs->isWindowed() || count($qs->getSortFields()))
+                    $sql = "($sql)";
                 $unions .= ' UNION '.($all ? 'ALL ' : '').$sql;
             }
         }
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index 7f4be6a37ec1ff5b0269cccc67b64d3883a7a672..190e0c3d37005a0c3a870bd8ab82790e91b9cb81 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -83,7 +83,7 @@ class PageNate {
         } else {
             $to= $this->total;
         }
-        $html=" ".__('Showing')."  ";
+        $html=__('Showing')." ";
         if ($this->total > 0) {
             $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */),
                $start, $end, $this->total);
diff --git a/include/class.search.php b/include/class.search.php
index a64d216c9a1d6913d441717bbda00c272adcfa72..8965f2e45f7f52856d2ed728d4e6ad0c9c4dac59 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -329,13 +329,14 @@ class MysqlSearchBackend extends SearchBackend {
 
         $criteria = clone $criteria;
 
-        $mode = ' IN BOOLEAN MODE';
+        $mode = ' IN NATURAL LANGUAGE MODE';
+        // If using boolean operators, search in boolean mode
+        if (preg_match('/["+<>(~-]\w|\w["*)]/u', $query, $T = array()))
+            $mode = ' IN BOOLEAN MODE';
         #if (count(explode(' ', $query)) == 1)
         #    $mode = ' WITH QUERY EXPANSION';
         $query = $this->quote($query);
-        $search = 'MATCH (search.title, search.content) AGAINST ('
-            .db_input($query)
-            .$mode.')';
+        $search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')';
         $tables = array();
         $P = TABLE_PREFIX;
         $sort = '';
@@ -343,29 +344,18 @@ class MysqlSearchBackend extends SearchBackend {
         switch ($criteria->model) {
         case false:
         case 'TicketModel':
-            if ($query) {
-            $key = 'COALESCE(Z1.ticket_id, Z2.ticket_id)';
             $criteria->extra(array(
                 'select' => array(
-                    'ticket_id' => $key,
-                    'relevance'=>'`search`.`relevance`',
+                    '__relevance__' => 'Z1.`relevance`',
                 ),
-                'order_by' => array(new SqlCode('`relevance`')),
                 'tables' => array(
-                    "(SELECT object_type, object_id, $search AS `relevance`
-                        FROM `{$P}_search` `search` WHERE $search) `search`",
-                    "(select ticket_id as ticket_id from {$P}ticket
-                ) Z1 ON (Z1.ticket_id = search.object_id and search.object_type = 'T')",
-                    "(select A3.id as thread_id, A1.ticket_id from {$P}ticket A1
-                    join {$P}thread A2 on (A1.ticket_id = A2.object_id and A2.object_type = 'T')
-                    join {$P}thread_entry A3 on (A2.id = A3.thread_id)
-                ) Z2 ON (Z2.thread_id = search.object_id and search.object_type = 'H')",
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                        "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`) as `ticket_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) WHERE {}) Z1"),
                 )
             ));
             // XXX: This is extremely ugly
-            $criteria->filter(array('ticket_id'=>new SqlCode($key)));
-            $criteria->distinct('ticket_id');
-            }
+            $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')))->distinct('ticket_id');
+
             // TODO: Consider sorting preferences
         }
 
@@ -800,7 +790,7 @@ class SavedSearch extends VerySimpleModel {
         }
         return $pieces;
     }
-    
+
     /**
      * Collect information on the search form.
      *
@@ -837,7 +827,7 @@ class SavedSearch extends VerySimpleModel {
         }
         return $info;
     }
-    
+
     /**
      * Get a description of a field in a search. Expects an entry from the
      * array retrieved in ::getSearchFields()
@@ -850,7 +840,7 @@ class SavedSearch extends VerySimpleModel {
         $form = $form ?: $this->getForm();
         $searchable = $this->getCurrentSearchFields($form->state);
         $qs = clone $qs;
-        
+
         // Figure out fields to search on
         foreach ($this->getSearchFields($form) as $name=>$info) {
             if (!$info['active'])
@@ -1168,4 +1158,16 @@ class TicketStatusChoiceField extends SelectionField {
             '!includes' =>  __('is not'),
         );
     }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__in" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__in" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 58363036bd02b50c5065ccecf5b141d875b86167..616d3a8a22e9ddbff8a71abe97a41f9c14cbd879 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -42,7 +42,6 @@ case 'closed':
     $status='closed';
     $results_type=__('Closed Tickets');
     $showassigned=true; //closed by.
-    $tickets->values('staff__firstname', 'staff__lastname', 'team__name');
     $queue_sort_options = array('closed', 'priority,due', 'due',
         'priority,updated', 'priority,created', 'answered', 'number', 'hot');
     break;
@@ -91,19 +90,19 @@ case 'search':
                 'user__emails__address__contains' => $_REQUEST['query'],
                 'user__org__name__contains' => $_REQUEST['query'],
             ));
+            $tickets->filter($basic_search);
             if (!$_REQUEST['search-type']) {
                 // [Search] click, consider keywords too. This is a
                 // relatively ugly hack. SearchBackend::find() add in a
                 // constraint for the search. We need to pop that off and
                 // include it as an OR with the above constraints
-                $tickets = $ost->searcher->find($_REQUEST['query'], $tickets);
-                $keywords = array_pop($tickets->constraints);
-                $basic_search->add($keywords);
-                // FIXME: The subquery technique below will crash with
-                //        keyword search
-                $use_subquery = false;
+                $keywords = TicketModel::objects();
+                $keywords->extra(array('select' => array('ticket_id' => 'Z1.ticket_id')));
+                $keywords = $ost->searcher->find($_REQUEST['query'], $keywords);
+                $tickets->values('ticket_id')->annotate(array('__relevance__' => new SqlCode(0.5)));
+                $keywords->aggregated = true; // Hack to prevent select ticket.*
+                $tickets->union($keywords)->order_by(new SqlCode('__relevance__'), QuerySet::DESC);
             }
-            $tickets->filter($basic_search);
         }
         // Clear sticky search queue
         unset($_SESSION[$queue_key]);
@@ -114,21 +113,12 @@ case 'search':
         $view_all_tickets = $thisstaff->hasPerm(SearchBackend::PERM_EVERYTHING);
         $results_type=__('Advanced Search')
             . '<a class="action-button" href="?clear_filter"><i style="top:0" class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>';
-        $has_relevance = false;
-        foreach ($tickets->getSortFields() as $sf) {
-            if ($sf instanceof SqlCode && $sf->code == '`relevance`') {
+        foreach ($form->getFields() as $sf) {
+            if ($sf->get('name') == 'keywords' && $sf->getClean()) {
                 $has_relevance = true;
                 break;
             }
         }
-        if ($has_relevance) {
-            $use_subquery = false;
-            array_unshift($queue_sort_options, 'relevance');
-        }
-        elseif ($_SESSION[$queue_sort_key] == 'relevance') {
-            unset($_SESSION[$queue_sort_key]);
-        }
-
         break;
     }
     // Apply user filter
@@ -165,8 +155,6 @@ if ($status != 'closed' && $queue_name != 'assigned') {
     $showassigned = !$hideassigned;
     if ($status == 'open' && $hideassigned)
         $tickets->filter(array('staff_id'=>0, 'team_id'=>0));
-    else
-        $tickets->values('staff__firstname', 'staff__lastname', 'team__name');
 }
 
 // Apply primary ticket status
@@ -202,9 +190,29 @@ $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $pageNav->setURL('tickets.php', $args);
 $tickets = $pageNav->paginate($tickets);
 
+// Rewrite $tickets to use a nested query, which will include the LIMIT part
+// in order to speed the result
+//
+// ATM, advanced search with keywords doesn't support the subquery approach
+if ($use_subquery) {
+    $orig_tickets = clone $tickets;
+    $tickets2 = TicketModel::objects();
+    $tickets2->values = $tickets->values;
+    $tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id')));
+
+    // Transfer the order_by from the original tickets
+    $tickets2->order_by($orig_tickets->getSortFields());
+    $tickets = $tickets2;
+}
+
 // Apply requested sorting
 $queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TICKET, $queue_name);
 
+// If relevance is available, use it as the default
+if ($has_relevance) {
+    array_unshift($queue_sort_options, 'relevance');
+}
+
 if (isset($_GET['sort'])) {
     $_SESSION[$queue_sort_key] = array($_GET['sort'], $_GET['dir']);
 }
@@ -215,6 +223,7 @@ elseif (!isset($_SESSION[$queue_sort_key])) {
 list($sort_cols, $sort_dir) = $_SESSION[$queue_sort_key];
 $orm_dir = $sort_dir ? QuerySet::ASC : QuerySet::DESC;
 $orm_dir_r = $sort_dir ? QuerySet::DESC : QuerySet::ASC;
+
 switch ($sort_cols) {
 case 'number':
     $tickets->extra(array(
@@ -267,7 +276,7 @@ case 'hot':
     break;
 
 case 'relevance':
-    $tickets->order_by(new SqlCode('relevance'), $orm_dir);
+    $tickets->order_by(new SqlCode('__relevance__'), $orm_dir);
     break;
 
 default:
@@ -284,27 +293,11 @@ case 'updated':
 // Save the query to the session for exporting
 $_SESSION[':Q:tickets'] = $tickets;
 
-// Rewrite $tickets to use a nested query, which will include the LIMIT part
-// in order to speed the result
-//
-// ATM, advanced search with keywords doesn't support the subquery approach
-if ($use_subquery) {
-    $orig_tickets = clone $tickets;
-    $tickets2 = TicketModel::objects();
-    $tickets2->values = $tickets->values;
-    $tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id')));
-
-    // Transfer the order_by from the original tickets
-    $tickets2->order_by($tickets->getSortFields());
-    $tickets = $tickets2;
-}
-
 TicketForm::ensureDynamicDataView();
 
 // Select pertinent columns
 // ------------------------------------------------------------
-$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__priority__priority_color', 'cdata__priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate', 'isanswered');
-
+$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__priority__priority_color', 'cdata__priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate', 'isanswered', 'staff__firstname', 'staff__lastname', 'team__name');
 // Add in annotations
 $tickets->annotate(array(
     'collab_count' => TicketThread::objects()
@@ -341,7 +334,7 @@ return false;">
     <input type="hidden" name="search-type" value=""/>
     <div class="attached input">
       <input type="text" class="basic-search" data-url="ajax.php/tickets/lookup" name="query"
-        autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query'], true); ?>"
+        autofocus size="30" value="<?php echo Format::htmlchars(urldecode($_REQUEST['query']), true); ?>"
         autocomplete="off" autocorrect="off" autocapitalize="off">
       <button type="submit" class="attached button"><i class="icon-search"></i>
       </button>
@@ -360,7 +353,7 @@ return false;">
         <div class="pull-left flush-left">
             <h2><a href="<?php echo $refresh_url; ?>"
                 title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo
-                $results_type.$showing; ?></a></h2>
+                $results_type; ?></a></h2>
         </div>
         <div class="pull-right flush-right">
             <?php