diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index 041678c0b1166374afc8715030029cf838695a1c..0cf4f22d6ad391afff7543079da762f14113b314 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -29,6 +29,9 @@ class OrgsAjaxAPI extends AjaxController {
         $q = $_REQUEST['q'];
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
 
+        if (strlen($q) < 2)
+            return $this->encode(array());
+
         $orgs = Organization::objects()
             ->values_flat('id', 'name')
             ->limit($limit);
@@ -38,7 +41,7 @@ class OrgsAjaxAPI extends AjaxController {
         $orgs->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
             ->distinct('id');
 
-        if (!count($orgs) && substr($q, strlen($q)-1) != '*') {
+        if (!count($orgs) && preg_match('`\w$`u', $q)) {
             // Do wildcard full-text search
             $_REQUEST['q'] = $q."*";
             return $this->search($type);
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 31457ec4f80497849129f1a5f126ac7d8a4c21fb..35bb513e973f3991c3dc729e4b19f4926f33ab39 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -47,8 +47,9 @@ class TicketsAjaxAPI extends AjaxController {
             ->limit($limit);
 
         $q = $_REQUEST['q'];
-        // Drop at sign in email addresses
-        $q = str_replace('@', ' ', $q);
+
+        if (strlen($q) < 2)
+            return $this->encode(array());
 
         global $ost;
         $hits = $ost->searcher->find($q, $hits)
@@ -66,7 +67,7 @@ class TicketsAjaxAPI extends AjaxController {
                 ->limit($limit)
                 ->union($hits);
         }
-        elseif (!count($hits) && $q[strlen($q)-1] != '*') {
+        elseif (!count($hits) && preg_match('`\w$`u', $q)) {
             // Do wild-card fulltext search
             $_REQUEST['q'] = $q.'*';
             return $this->lookup();
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 0dd6da74b3384f14d16800c6c36ca657220cf7d6..bb94a1528992b9fb991d3f3e399de5dcefcd8bdb 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -34,6 +34,9 @@ class UsersAjaxAPI extends AjaxController {
         $users=array();
         $emails=array();
 
+        if (strlen($q) < 2)
+            return $this->encode(array());
+
         if (!$type || !strcasecmp($type, 'remote')) {
             foreach (AuthenticationBackend::searchUsers($q) as $u) {
                 $name = new UsersName(array('first' => $u['first'], 'last' => $u['last']));
@@ -55,7 +58,7 @@ class UsersAjaxAPI extends AjaxController {
             $users->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
                 ->distinct('id');
 
-            if (!count($emails) && !count($users) && substr($q, strlen($q)-1) != '*') {
+            if (!count($emails) && !count($users) && preg_match('`\w$`u', $q)) {
                 // Do wildcard full-text search
                 $_REQUEST['q'] = $q."*";
                 return $this->search($type);
diff --git a/include/class.search.php b/include/class.search.php
index 6cac6d7e4e9d51ee436589114633cb6fe7770af8..474fa3132d6c6763149c49c74893602c829fe26c 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -306,7 +306,7 @@ class MysqlSearchBackend extends SearchBackend {
     // Quote things like email addresses
     function quote($query) {
         $parts = array();
-        if (!preg_match_all('`([^\s"\']+)|"[^"]*"|\'[^\']*\'`', $query, $parts,
+        if (!preg_match_all('`(?:([^\s"\']+)|"[^"]*"|\'[^\']*\')(\s*)`', $query, $parts,
                 PREG_SET_ORDER))
             return $query;
 
@@ -319,21 +319,22 @@ class MysqlSearchBackend extends SearchBackend {
                 $char = strpos($m[1], '"') ? "'" : '"';
                 $m[0] = $char . $m[0] . $char;
             }
-            $results[] = $m[0];
+            $results[] = $m[0].$m[2];
         }
-        return implode(' ', $results);
+        return implode('', $results);
     }
 
     function find($query, QuerySet $criteria, $addRelevance=true) {
         global $thisstaff;
 
+        // MySQL usually doesn't handle words shorter than three letters
+        // (except with special configuration)
+        if (strlen($query) < 3)
+            return $criteria;
+
         $criteria = clone $criteria;
 
         $mode = ' IN NATURAL LANGUAGE MODE';
-        // If using boolean operators, search in boolean mode. This regex
-        // will ensure proper placement of operators, whitespace, and quotes
-        // in an effort to avoid crashing the query at MySQL
-        $query = $this->quote($query);
 
         // According to the MySQL full text boolean mode, this grammar is
         // assumed:
@@ -357,6 +358,10 @@ class MysqlSearchBackend extends SearchBackend {
         if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T = array())
             && preg_match("`^{$BOOLEAN}$`u", $query, $T = array())
         ) {
+            // If using boolean operators, search in boolean mode. This regex
+            // will ensure proper placement of operators, whitespace, and quotes
+            // in an effort to avoid crashing the query at MySQL
+            $query = $this->quote($query);
             $mode = ' IN BOOLEAN MODE';
         }
         #elseif (count(explode(' ', $query)) == 1)
@@ -376,7 +381,7 @@ class MysqlSearchBackend extends SearchBackend {
             $criteria->extra(array(
                 'tables' => array(
                     str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
-                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`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`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {}) Z1"),
+                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, SUM({}) 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`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {} GROUP BY `ticket_id`) Z1"),
                 )
             ));
             $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')));
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index cf3c5e1d92d1c83fd9b02a9bea18bc12f7e3470e..f37874b55a8916081c924599130f64c863d36f37 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -90,10 +90,10 @@ if ($thisclient->canSeeOrgTickets()) {
 
 // Perform basic search
 if ($settings['keywords']) {
-    $q = $settings['keywords'];
+    $q = trim($settings['keywords']);
     if (is_numeric($q)) {
         $tickets->filter(array('number__startswith'=>$q));
-    } else { //Deep search!
+    } elseif (strlen($q) > 2) { //Deep search!
         // Use the search engine to perform the search
         $tickets = $ost->searcher->find($q, $tickets);
     }
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 2bdb8ba5ecc81cad256df6d4fdbcbb59441bc442..690f6c2066fefa03c4b2e78b7e39a6919b08f610 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -93,18 +93,22 @@ case 'search':
                 ));
             }
         }
-        elseif ($_REQUEST['query']) {
+        elseif (isset($_REQUEST['query'])
+            && ($q = trim($_REQUEST['query']))
+            && strlen($q) > 2
+        ) {
             // [Search] click, consider keywords
-            $__tickets = $ost->searcher->find($_REQUEST['query'], $tickets);
-            if (!count($__tickets)) {
+            $__tickets = $ost->searcher->find($q, $tickets);
+            if (!count($__tickets) && preg_match('`\w$`u', $q)) {
                 // Do wildcard search if no hits
-                $__tickets = $ost->searcher->find($_REQUEST['query'].'*', $tickets);
+                $__tickets = $ost->searcher->find($q.'*', $tickets);
             }
-            $tickets = $__tickets->distinct('ticket_id');
+            $tickets = $__tickets;
             $has_relevance = true;
         }
         if (count($tickets) == 1) {
             // Redirect to ticket page
+            Http::redirect('tickets.php?id='.$tickets[0]->getId());
         }
         // Clear sticky search queue
         unset($_SESSION[$queue_key]);