diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 708bb6dbdc88b3f6adadba7205cc60efb5da8b4c..d5d12bea93f519e3b443038fbcdda61f35615566 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -97,184 +97,6 @@ class TicketsAjaxAPI extends AjaxController {
         return $this->json_encode($tickets);
     }
 
-    function _search($req) {
-        global $thisstaff, $cfg, $ost;
-
-        $result=array();
-        $criteria = array();
-
-        $select = 'SELECT ticket.ticket_id';
-        $from = ' FROM '.TICKET_TABLE.' ticket
-                  LEFT JOIN '.TICKET_STATUS_TABLE.' status
-                    ON (status.id = ticket.status_id) ';
-        //Access control.
-        $where = ' WHERE ( (ticket.staff_id='.db_input($thisstaff->getId())
-                    .' AND status.state="open" )';
-
-        if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-            $where.=' OR (ticket.team_id IN ('.implode(',', db_input(array_filter($teams)))
-                   .' ) AND status.state="open" )';
-
-        if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-            $where.=' OR ticket.dept_id IN ('.implode(',', db_input($depts)).')';
-
-        $where.=' ) ';
-
-        //Department
-        if ($req['deptId']) {
-            $where.=' AND ticket.dept_id='.db_input($req['deptId']);
-            $criteria['dept_id'] = $req['deptId'];
-        }
-
-        //Help topic
-        if($req['topicId']) {
-            $where.=' AND ticket.topic_id='.db_input($req['topicId']);
-            $criteria['topic_id'] = $req['topicId'];
-        }
-
-        // Status
-        if ($req['statusId']
-                && ($status=TicketStatus::lookup($req['statusId']))) {
-            $where .= sprintf(' AND status.id="%d" ',
-                    $status->getId());
-            $criteria['status_id'] = $status->getId();
-        }
-
-        // Flags
-        if ($req['flag']) {
-            switch (strtolower($req['flag'])) {
-                case 'answered':
-                    $where .= ' AND ticket.isanswered =1 ';
-                    $criteria['isanswered'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
-                case 'overdue':
-                    $where .= ' AND ticket.isoverdue =1 ';
-                    $criteria['isoverdue'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
-            }
-        }
-
-        //Assignee
-        if($req['assignee'] && strcasecmp($req['status'], 'closed'))  { # assigned-to
-            $id=preg_replace("/[^0-9]/", "", $req['assignee']);
-            $assignee = $req['assignee'];
-            $where.= ' AND ( ( status.state="open" ';
-            if($assignee[0]=='t') {
-                $where.=' AND ticket.team_id='.db_input($id);
-                $criteria['team_id'] = $id;
-            }
-            elseif($assignee[0]=='s' || is_numeric($id)) {
-                $where.=' AND ticket.staff_id='.db_input($id);
-                $criteria['staff_id'] = $id;
-            }
-
-            $where.=')';
-
-            if($req['staffId'] && !$req['status']) //Assigned TO + Closed By
-                $where.= ' OR (ticket.staff_id='.db_input($req['staffId']).
-                    ' AND status.state IN("closed")) ';
-            elseif($req['staffId']) // closed by any
-                $where.= ' OR status.state IN("closed") ';
-
-            $where.= ' ) ';
-        } elseif($req['staffId']) { # closed-by
-            $where.=' AND (ticket.staff_id='.db_input($req['staffId']).' AND
-                status.state IN("closed")) ';
-            $criteria['state__in'] = array('closed');
-            $criteria['staff_id'] = $req['staffId'];
-        }
-
-        //dates
-        $startTime  =($req['startDate'] && (strlen($req['startDate'])>=8))?strtotime($req['startDate']):0;
-        $endTime    =($req['endDate'] && (strlen($req['endDate'])>=8))?strtotime($req['endDate']):0;
-        if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0))
-            $startTime=$endTime=0;
-
-        if($startTime) {
-            $where.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')';
-            $criteria['created__gte'] = $startTime;
-        }
-
-        if($endTime) {
-            $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')';
-            $criteria['created__lte'] = $startTime;
-        }
-
-        // Dynamic fields
-        $cdata_search = false;
-        foreach (TicketForm::getInstance()->getFields() as $f) {
-            if (isset($req[$f->getFormName()])
-                    && ($val = $req[$f->getFormName()])) {
-                $name = $f->get('name') ? $f->get('name')
-                    : 'field_'.$f->get('id');
-                if (is_array($val)) {
-                    $cwhere = '(' . implode(' OR ', array_map(
-                        function($k) use ($name) {
-                            return sprintf('FIND_IN_SET(%s, `%s`)', db_input($k), $name);
-                        }, $val)
-                    ) . ')';
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                else {
-                    $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'";
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                $where .= ' AND ('.$cwhere.')';
-                $cdata_search = true;
-            }
-        }
-        if ($cdata_search)
-            $from .= 'LEFT JOIN '.TABLE_PREFIX.'ticket__cdata '
-                    ." cdata ON (cdata.ticket_id = ticket.ticket_id)";
-
-        //Query
-        $joins = array();
-        if($req['query']) {
-            // Setup sets of joins and queries
-            if ($s = $ost->searcher)
-               return $s->find($req['query'], $criteria, 'Ticket');
-        }
-
-        $sections = array();
-        foreach ($joins as $j) {
-            $sections[] = "$select $from {$j['from']} $where AND ({$j['where']})";
-        }
-        if (!$joins)
-            $sections[] = "$select $from $where";
-
-        $sql=implode(' union ', $sections);
-        if (!($res = db_query($sql)))
-            return TicketForm::dropDynamicDataView();
-
-        $tickets = array();
-        while ($row = db_fetch_row($res))
-            $tickets[] = $row[0];
-
-        return $tickets;
-    }
-
-    function search() {
-        $tickets = self::_search($_REQUEST);
-        $result = array();
-
-        if (count($tickets)) {
-            $uid = md5($_SERVER['QUERY_STRING']);
-            $_SESSION["adv_$uid"] = $tickets;
-            $result['success'] = sprintf(__("Search criteria matched %s"),
-                    sprintf(_N('%d ticket', '%d tickets', count($tickets)), count($tickets)
-                ))
-                . " - <a href='tickets.php?advsid=$uid'>".__('view')."</a>";
-        } else {
-            $result['fail']=__('No tickets found matching your search criteria.');
-        }
-
-        return $this->json_encode($result);
-    }
-
     function acquireLock($tid) {
         global $cfg,$thisstaff;
 
diff --git a/include/class.search.php b/include/class.search.php
index e7647cc3711330adfd9fa5566b895c686b231aa3..3a75ad09d950fcfefc36b53b49af9de5c2f989dc 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -31,7 +31,7 @@ abstract class SearchBackend {
     const SORT_OLDEST = 3;
 
     abstract function update($model, $id, $content, $new=false, $attrs=array());
-    abstract function find($query, $criteria, $model=false, $sort=array());
+    abstract function find($query, QuerySet $criteria);
 
     static function register($backend=false) {
         $backend = $backend ?: get_called_class();
@@ -61,9 +61,9 @@ class SearchInterface {
         $this->bootstrap();
     }
 
-    function find($query, $criteria, $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria) {
         $query = Format::searchable($query);
-        return $this->backend->find($query, $criteria, $model, $sort);
+        return $this->backend->find($query, $criteria);
     }
 
     function update($model, $id, $content, $new=false, $attrs=array()) {
@@ -278,7 +278,7 @@ class MysqlSearchBackend extends SearchBackend {
         return implode(' ', $results);
     }
 
-    function find($query, $criteria=array(), $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria) {
         global $thisstaff;
 
         $mode = ' IN BOOLEAN MODE';
@@ -292,63 +292,32 @@ class MysqlSearchBackend extends SearchBackend {
         $P = TABLE_PREFIX;
         $sort = '';
 
-        if ($query) {
-            $tables[] = "(
-                SELECT object_type, object_id, $search AS `relevance`
-                FROM `{$P}_search` `search`
-                WHERE $search
-            ) `search`";
-            $sort = 'ORDER BY `search`.`relevance`';
-        }
-
-        switch ($model) {
+        switch ($criteria->model) {
         case false:
-        case 'Ticket':
-            $tables[] = "(select ticket_id as ticket_id from {$P}ticket
-            ) B1 ON (B1.ticket_id = search.object_id and search.object_type = 'T')";
-            $tables[] = "(select A2.id as thread_id, A1.ticket_id from {$P}ticket A1
-                join {$P}ticket_thread A2 on (A1.ticket_id = A2.ticket_id)
-            ) B2 ON (B2.thread_id = search.object_id and search.object_type = 'H')";
-            $tables[] = "(select A3.id as user_id, A1.ticket_id from {$P}user A3
-                join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B3 ON (B3.user_id = search.object_id and search.object_type = 'U')";
-            $tables[] = "(select A4.id as org_id, A1.ticket_id from {$P}organization A4
-                join {$P}user A3 on (A3.org_id = A4.id) join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B4 ON (B4.org_id = search.object_id and search.object_type = 'O')";
-            $key = 'COALESCE(B1.ticket_id, B2.ticket_id, B3.ticket_id, B4.ticket_id)';
-            $tables[] = "{$P}ticket A1 ON (A1.ticket_id = {$key})";
-            $tables[] = "{$P}ticket_status A2 ON (A1.status_id = A2.id)";
-            $cdata_search = false;
-            $where = array();
-
-            if ($criteria) {
-                foreach ($criteria as $name=>$value) {
-                    switch ($name) {
-                    case 'status_id':
-                        $where[] = 'A2.id = '.db_input($value);
-                        break;
-                    case 'state':
-                        $where[] = 'A2.state = '.db_input($value);
-                        break;
-                    case 'state__in':
-                        $where[] = 'A2.state IN ('.implode(',',db_input($value)).')';
-                        break;
-                    case 'topic_id':
-                    case 'staff_id':
-                    case 'team_id':
-                    case 'dept_id':
-                    case 'user_id':
-                    case 'isanswered':
-                    case 'isoverdue':
-                    case 'number':
-                        $where[] = sprintf('A1.%s = %s', $name, db_input($value));
-                        break;
-                    case 'created__gte':
-                        $where[] = sprintf('A1.created >= %s', db_input($value));
-                        break;
-                    case 'created__lte':
-                        $where[] = sprintf('A1.created <= %s', db_input($value));
-                        break;
+        case 'TicketModel':
+            if ($query) {
+            $key = 'COALESCE(Z1.ticket_id, Z2.ticket_id)';
+            $criteria->extra(array(
+                'select' => array(
+                    'key' => $key,
+                    'relevance'=>'`search`.`relevance`',
+                ),
+                'order_by' => array('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 A2.id as thread_id, A1.ticket_id from {$P}ticket A1
+                    join {$P}ticket_thread A2 on (A1.ticket_id = A2.ticket_id)
+                ) Z2 ON (Z2.thread_id = search.object_id and search.object_type = 'H')",
+                )
+            ));
+            // XXX: This is extremely ugly
+            $criteria->filter(array('ticket_id'=>new SqlCode($key)));
+            $criteria->distinct('ticket_id');
+            }
+            /*
                     case 'email':
                     case 'org_id':
                     case 'form_id':
@@ -371,57 +340,17 @@ class MysqlSearchBackend extends SearchBackend {
                         }
                     }
                 }
-            }
-            if ($cdata_search)
-                $tables[] = TABLE_PREFIX.'ticket__cdata cdata'
-                    .' ON (cdata.ticket_id = A1.ticket_id)';
-
-            // Always consider the current staff's access
-            $thisstaff->getDepts();
-            $access = array();
-            $access[] = '(A1.staff_id=' . db_input($thisstaff->getId())
-                .' AND A2.state="open")';
-
-            if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-                $access[] = 'A1.dept_id IN ('
-                    . ($depts ? implode(',', db_input($depts)) : 0)
-                    . ')';
-
-            if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
-                $access[] = 'A1.team_id IN ('
-                    .implode(',', db_input(array_filter($teams)))
-                    .') AND A2.state="open"';
-
-            $where[] = '(' . implode(' OR ', $access) . ')';
+             */
 
             // TODO: Consider sorting preferences
-
-            $sql = 'SELECT DISTINCT '
-                . $key
-                . ' FROM '
-                . implode(' LEFT JOIN ', $tables)
-                . ' WHERE ' . implode(' AND ', $where)
-                . $sort
-                . ' LIMIT 500';
         }
 
-        $class = get_class();
-        $auto_create = function($db_error) use ($class) {
-
-            if ($db_error != 1146)
-                // Perform the standard error handling
-                return true;
-
+        // TODO: Ensure search table exists;
+        if (false) {
             // Create the search table automatically
             $class::createSearchTable();
-        };
-        $res = db_query($sql, $auto_create);
-        $object_ids = array();
-
-        while ($row = db_fetch_row($res))
-            $object_ids[] = $row[0];
-
-        return $object_ids;
+        }
+        return $criteria;
     }
 
     static function createSearchTable() {
@@ -687,6 +616,9 @@ class SavedSearch extends VerySimpleModel {
             foreach ($form->getFields() as $F) {
                 if (substr($F->get('name'), -7) == '+search' && $F->getClean())
                     $selected += 1;
+                // Consider keyword searches
+                elseif ($F->get('name') == 'keywords' && $F->getClean())
+                    $selected += 1;
             }
             if (!$selected)
                 $form->addError('No fields selected for searching');
@@ -778,6 +710,14 @@ class SavedSearch extends VerySimpleModel {
                 }
             }
         }
+
+        // Consider keyword searching
+        if ($keywords = $form->getField('keywords')->getClean()) {
+            global $ost;
+
+            $qs = $ost->searcher->find($keywords, $qs);
+        }
+
         return $qs;
     }