diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php
index b224dde4d..b130dbf2a 100644
--- a/src/applications/diviner/atom/DivinerAtom.php
+++ b/src/applications/diviner/atom/DivinerAtom.php
@@ -1,426 +1,437 @@
 <?php
 
 final class DivinerAtom {
 
   const TYPE_FILE      = 'file';
   const TYPE_ARTICLE   = 'article';
   const TYPE_METHOD    = 'method';
   const TYPE_CLASS     = 'class';
   const TYPE_FUNCTION  = 'function';
   const TYPE_INTERFACE = 'interface';
 
   private $type;
   private $name;
   private $file;
   private $line;
   private $hash;
   private $contentRaw;
   private $length;
   private $language;
   private $docblockRaw;
   private $docblockText;
   private $docblockMeta;
   private $warnings = array();
   private $parent;
   private $parentHash;
   private $children = array();
   private $childHashes = array();
   private $context;
   private $extends = array();
   private $links = array();
   private $book;
   private $properties = array();
 
   /**
    * Returns a sorting key which imposes an unambiguous, stable order on atoms.
    */
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getBook(),
         $this->getType(),
         $this->getContext(),
         $this->getName(),
         $this->getFile(),
         sprintf('%08', $this->getLine()),
       ));
   }
 
   public function setBook($book) {
     $this->book = $book;
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public static function getAtomSerializationVersion() {
     return 2;
   }
 
   public function addWarning($warning) {
     $this->warnings[] = $warning;
     return $this;
   }
 
   public function getWarnings() {
     return $this->warnings;
   }
 
   public function setDocblockRaw($docblock_raw) {
     $this->docblockRaw = $docblock_raw;
 
     $parser = new PhutilDocblockParser();
     list($text, $meta) = $parser->parse($docblock_raw);
     $this->docblockText = $text;
     $this->docblockMeta = $meta;
 
     return $this;
   }
 
   public function getDocblockRaw() {
     return $this->docblockRaw;
   }
 
   public function getDocblockText() {
     if ($this->docblockText === null) {
       throw new Exception("Call setDocblockRaw() before getDocblockText()!");
     }
     return $this->docblockText;
   }
 
   public function getDocblockMeta() {
     if ($this->docblockMeta === null) {
       throw new Exception("Call setDocblockRaw() before getDocblockMeta()!");
     }
     return $this->docblockMeta;
   }
 
   public function getDocblockMetaValue($key, $default = null) {
     $meta = $this->getDocblockMeta();
     return idx($meta, $key, $default);
   }
 
   public function setDocblockMetaValue($key, $value) {
     $meta = $this->getDocblockMeta();
     $meta[$key] = $value;
     $this->docblockMeta = $meta;
     return $this;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setFile($file) {
     $this->file = $file;
     return $this;
   }
 
   public function getFile() {
     return $this->file;
   }
 
   public function setLine($line) {
     $this->line = $line;
     return $this;
   }
 
   public function getLine() {
     return $this->line;
   }
 
   public function setContentRaw($content_raw) {
     $this->contentRaw = $content_raw;
     return $this;
   }
 
   public function getContentRaw() {
     return $this->contentRaw;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function addLink(DivinerAtomRef $ref) {
     $this->links[] = $ref;
     return $this;
   }
 
   public function addExtends(DivinerAtomRef $ref) {
     $this->extends[] = $ref;
     return $this;
   }
 
   public function getLinkDictionaries() {
     return mpull($this->links, 'toDictionary');
   }
 
   public function getExtendsDictionaries() {
     return mpull($this->extends, 'toDictionary');
   }
 
   public function getExtends() {
     return $this->extends;
   }
 
   public function getHash() {
     if ($this->hash) {
       return $this->hash;
     }
 
     $parts = array(
       $this->getBook(),
       $this->getType(),
       $this->getName(),
       $this->getFile(),
       $this->getLine(),
       $this->getLength(),
       $this->getLanguage(),
       $this->getContentRaw(),
       $this->getDocblockRaw(),
       $this->getProperties(),
       $this->getChildHashes(),
       mpull($this->extends, 'toHash'),
       mpull($this->links, 'toHash'),
     );
 
     $this->hash = md5(serialize($parts)).'N';
     return $this->hash;
   }
 
   public function setLength($length) {
     $this->length = $length;
     return $this;
   }
 
   public function getLength() {
     return $this->length;
   }
 
   public function setLanguage($language) {
     $this->language = $language;
     return $this;
   }
 
   public function getLanguage() {
     return $this->language;
   }
 
   public function addChildHash($child_hash) {
     $this->childHashes[] = $child_hash;
     return $this;
   }
 
   public function getChildHashes() {
     if (!$this->childHashes && $this->children) {
       $this->childHashes = mpull($this->children, 'getHash');
     }
     return $this->childHashes;
   }
 
   public function setParentHash($parent_hash) {
     if ($this->parentHash) {
       throw new Exception("Atom already has a parent!");
     }
     $this->parentHash = $parent_hash;
     return $this;
   }
 
   public function hasParent() {
     return $this->parent || $this->parentHash;
   }
 
   public function setParent(DivinerAtom $atom) {
     if ($this->parentHash) {
       throw new Exception("Parent hash has already been computed!");
     }
     $this->parent = $atom;
     return $this;
   }
 
   public function getParentHash() {
     if ($this->parent && !$this->parentHash) {
       $this->parentHash = $this->parent->getHash();
     }
     return $this->parentHash;
   }
 
   public function addChild(DivinerAtom $atom) {
     if ($this->childHashes) {
       throw new Exception("Child hashes have already been computed!");
     }
 
     $atom->setParent($this);
     $this->children[] = $atom;
     return $this;
   }
 
   public function getURI() {
     $parts = array();
     $parts[] = phutil_escape_uri_path_component($this->getType());
     if ($this->getContext()) {
       $parts[] = phutil_escape_uri_path_component($this->getContext());
     }
     $parts[] = phutil_escape_uri_path_component($this->getName());
     $parts[] = null;
     return implode('/', $parts);
   }
 
 
   public function toDictionary() {
     // NOTE: If you change this format, bump the format version in
     // getAtomSerializationVersion().
 
     return array(
       'book'        => $this->getBook(),
       'type'        => $this->getType(),
       'name'        => $this->getName(),
       'file'        => $this->getFile(),
       'line'        => $this->getLine(),
       'hash'        => $this->getHash(),
       'uri'         => $this->getURI(),
       'length'      => $this->getLength(),
       'context'     => $this->getContext(),
       'language'    => $this->getLanguage(),
       'docblockRaw' => $this->getDocblockRaw(),
       'warnings'    => $this->getWarnings(),
       'parentHash'  => $this->getParentHash(),
       'childHashes' => $this->getChildHashes(),
       'extends'     => $this->getExtendsDictionaries(),
       'links'       => $this->getLinkDictionaries(),
       'ref'         => $this->getRef()->toDictionary(),
       'properties'  => $this->getProperties(),
     );
   }
 
   public function getRef() {
     $title = null;
     if ($this->docblockMeta) {
       $title = $this->getDocblockMetaValue('title');
     }
 
     return id(new DivinerAtomRef())
       ->setBook($this->getBook())
       ->setContext($this->getContext())
       ->setType($this->getType())
       ->setName($this->getName())
       ->setTitle($title)
       ->setGroup($this->getProperty('group'));
   }
 
   public static function newFromDictionary(array $dictionary) {
     $atom = id(new DivinerAtom())
       ->setBook(idx($dictionary, 'book'))
       ->setType(idx($dictionary, 'type'))
       ->setName(idx($dictionary, 'name'))
       ->setFile(idx($dictionary, 'file'))
       ->setLine(idx($dictionary, 'line'))
       ->setHash(idx($dictionary, 'hash'))
       ->setLength(idx($dictionary, 'length'))
       ->setContext(idx($dictionary, 'context'))
       ->setLanguage(idx($dictionary, 'language'))
       ->setParentHash(idx($dictionary, 'parentHash'))
       ->setDocblockRaw(idx($dictionary, 'docblockRaw'))
       ->setProperties(idx($dictionary, 'properties'));
 
     foreach (idx($dictionary, 'warnings', array()) as $warning) {
       $atom->addWarning($warning);
     }
 
     foreach (idx($dictionary, 'childHashes', array()) as $child) {
       $atom->addChildHash($child);
     }
 
     foreach (idx($dictionary, 'extends', array()) as $extends) {
       $atom->addExtends(DivinerAtomRef::newFromDictionary($extends));
     }
 
     return $atom;
   }
 
   public function getProperty($key, $default = null) {
     return idx($this->properties, $key, $default);
   }
 
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
   }
 
   public function getProperties() {
     return $this->properties;
   }
 
   public function setProperties(array $properties) {
     $this->properties = $properties;
     return $this;
   }
 
   public static function getThisAtomIsNotDocumentedString($type) {
     switch ($type) {
       case self::TYPE_FILE:
         return pht('This file is not documented.');
       case self::TYPE_FUNCTION:
         return pht('This function is not documented.');
       case self::TYPE_CLASS:
         return pht('This class is not documented.');
       case self::TYPE_ARTICLE:
         return pht('This article is not documented.');
       case self::TYPE_METHOD:
         return pht('This method is not documented.');
       case self::TYPE_INTERFACE:
         return pht('This interface is not documented.');
       default:
         phlog("Need translation for '{$type}'.");
         return pht('This %s is not documented.', $type);
     }
   }
 
+  public static function getAllTypes() {
+    return array(
+      self::TYPE_FILE,
+      self::TYPE_FUNCTION,
+      self::TYPE_CLASS,
+      self::TYPE_ARTICLE,
+      self::TYPE_METHOD,
+      self::TYPE_INTERFACE,
+    );
+  }
+
   public static function getAtomTypeNameString($type) {
     switch ($type) {
       case self::TYPE_FILE:
         return pht('File');
       case self::TYPE_FUNCTION:
         return pht('Function');
       case self::TYPE_CLASS:
         return pht('Class');
       case self::TYPE_ARTICLE:
         return pht('Article');
       case self::TYPE_METHOD:
         return pht('Method');
       case self::TYPE_INTERFACE:
         return pht('Interface');
       default:
         phlog("Need translation for '{$type}'.");
         return ucwords($type);
     }
   }
 
 }
diff --git a/src/applications/diviner/atom/DivinerAtomRef.php b/src/applications/diviner/atom/DivinerAtomRef.php
index b536ed622..87e2ce940 100644
--- a/src/applications/diviner/atom/DivinerAtomRef.php
+++ b/src/applications/diviner/atom/DivinerAtomRef.php
@@ -1,206 +1,210 @@
 <?php
 
 final class DivinerAtomRef {
 
   private $book;
   private $context;
   private $type;
   private $name;
   private $group;
   private $summary;
   private $index;
   private $title;
 
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getName(),
         $this->getType(),
         $this->getContext(),
         $this->getBook(),
         $this->getIndex(),
       ));
   }
 
   public function setIndex($index) {
     $this->index = $index;
     return $this;
   }
 
   public function getIndex() {
     return $this->index;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     return $this->summary;
   }
 
   public function setName($name) {
     $normal_name = self::normalizeString($name);
     if (preg_match('/^@[0-9]+$/', $normal_name)) {
       throw new Exception(
         "Atom names must not be in the form '/@\d+/'. This pattern is ".
         "reserved for disambiguating atoms with similar names.");
     }
     $this->name = $normal_name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setType($type) {
     $this->type = self::normalizeString($type);
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setContext($context) {
     if ($context === null) {
       $this->context = $context;
     } else {
       $this->context = self::normalizeString($context);
     }
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public function setBook($book) {
     if ($book === null) {
       $this->book = $book;
     } else {
       $this->book = self::normalizeString($book);
     }
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setGroup($group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     return $this->group;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function getTitleSlug() {
     return self::normalizeTitleString($this->getTitle());
   }
 
   public function toDictionary() {
     return array(
       'book'    => $this->getBook(),
       'context' => $this->getContext(),
       'type'    => $this->getType(),
       'name'    => $this->getName(),
       'group'   => $this->getGroup(),
       'index'   => $this->getIndex(),
       'summary' => $this->getSummary(),
       'title'   => $this->getTitle(),
     );
   }
 
   public function toHash() {
     $dict = $this->toDictionary();
 
     unset($dict['group']);
     unset($dict['index']);
     unset($dict['summary']);
     unset($dict['title']);
 
     ksort($dict);
     return md5(serialize($dict)).'S';
   }
 
   public static function newFromDictionary(array $dict) {
     $obj = new DivinerAtomRef();
     $obj->setBook(idx($dict, 'book'));
     $obj->setContext(idx($dict, 'context'));
     $obj->setType(idx($dict, 'type'));
     $obj->setName(idx($dict, 'name'));
     $obj->group = idx($dict, 'group');
     $obj->index = idx($dict, 'index');
     $obj->summary = idx($dict, 'summary');
     $obj->title = idx($dict, 'title');
 
     return $obj;
   }
 
   public static function normalizeString($str) {
     // These characters create problems on the filesystem or in URIs. Replace
     // them with non-problematic appoximations (instead of simply removing them)
     // to keep the URIs fairly useful and avoid unnecessary collisions. These
     // approximations are selected based on some domain knowledge of common
     // languages: where a character is used as a delimiter, it is more helpful
     // to replace it with a "." or a ":" or similar, while it's better if
     // operator overloads read as, e.g., "operator_div".
 
     $map = array(
       // Hopefully not used anywhere by anything.
       '#'   => '.',
 
       // Used in Ruby methods.
       '?'   => 'Q',
 
       // Used in PHP namespaces.
       '\\'  => '.',
 
       // Used in "operator +" in C++.
       '+'   => 'plus',
 
       // Used in "operator %" in C++.
       '%'   => 'mod',
 
       // Used in "operator /" in C++.
       '/'   => 'div',
     );
     $str = str_replace(array_keys($map), array_values($map), $str);
 
     // Replace all spaces with underscores.
     $str = preg_replace('/ +/', '_', $str);
 
     // Replace control characters with "X".
     $str = preg_replace('/[\x00-\x19]/', 'X', $str);
 
     // Replace specific problematic names with alternative names.
     $alternates = array(
       '.'   => 'dot',
       '..'  => 'dotdot',
       ''    => 'null',
     );
 
     return idx($alternates, $str, $str);
   }
 
   public static function normalizeTitleString($str) {
+    // Remove colons from titles. This is mostly to accommodate legacy rules
+    // from the old Diviner, which generated a significant number of article
+    // URIs without colons present in the titles.
+    $str = str_replace(':', '', $str);
     $str = self::normalizeString($str);
     return phutil_utf8_strtolower($str);
   }
 
 }
diff --git a/src/applications/diviner/controller/DivinerAtomListController.php b/src/applications/diviner/controller/DivinerAtomListController.php
index 0da8842e5..1bddea865 100644
--- a/src/applications/diviner/controller/DivinerAtomListController.php
+++ b/src/applications/diviner/controller/DivinerAtomListController.php
@@ -1,32 +1,54 @@
 <?php
 
 final class DivinerAtomListController extends DivinerController
   implements PhabricatorApplicationSearchResultsControllerInterface {
 
   private $key;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function willProcessRequest(array $data) {
     $this->key = idx($data, 'key', 'all');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $controller = id(new PhabricatorApplicationSearchController($request))
       ->setQueryKey($this->key)
       ->setSearchEngine(new DivinerAtomSearchEngine())
       ->setNavigation($this->buildSideNavView());
 
     return $this->delegateToController($controller);
   }
 
   public function renderResultsList(
     array $symbols,
     PhabricatorSavedQuery $query) {
-    return $this->renderAtomList($symbols);
+
+    assert_instances_of($symbols, 'DivinerLiveSymbol');
+
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    $list = id(new PHUIObjectItemListView())
+      ->setUser($viewer);
+
+    foreach ($symbols as $symbol) {
+      $type = $symbol->getType();
+      $type_name = DivinerAtom::getAtomTypeNameString($type);
+
+      $item = id(new PHUIObjectItemView())
+        ->setHeader($symbol->getTitle())
+        ->setHref($symbol->getURI())
+        ->addAttribute($symbol->getSummary())
+        ->addIcon('none', $type_name);
+
+      $list->addItem($item);
+    }
+
+    return $list;
   }
 
 }
diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php
index df6124293..6dc3ff076 100644
--- a/src/applications/diviner/query/DivinerAtomQuery.php
+++ b/src/applications/diviner/query/DivinerAtomQuery.php
@@ -1,432 +1,449 @@
 <?php
 
 final class DivinerAtomQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $bookPHIDs;
   private $names;
   private $types;
   private $contexts;
   private $indexes;
   private $includeUndocumentable;
   private $includeGhosts;
   private $nodeHashes;
   private $titles;
+  private $nameContains;
 
   private $needAtoms;
   private $needExtends;
   private $needChildren;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withBookPHIDs(array $phids) {
     $this->bookPHIDs = $phids;
     return $this;
   }
 
   public function withTypes(array $types) {
     $this->types = $types;
     return $this;
   }
 
   public function withNames(array $names) {
     $this->names = $names;
     return $this;
   }
 
   public function withContexts(array $contexts) {
     $this->contexts = $contexts;
     return $this;
   }
 
   public function withIndexes(array $indexes) {
     $this->indexes = $indexes;
     return $this;
   }
 
   public function withNodeHashes(array $hashes) {
     $this->nodeHashes = $hashes;
     return $this;
   }
 
   public function withTitles($titles) {
     $this->titles = $titles;
     return $this;
   }
 
+  public function withNameContains($text) {
+    $this->nameContains = $text;
+    return $this;
+  }
+
   public function needAtoms($need) {
     $this->needAtoms = $need;
     return $this;
   }
 
   public function needChildren($need) {
     $this->needChildren = $need;
     return $this;
   }
 
 
   /**
    * Include "ghosts", which are symbols which used to exist but do not exist
    * currently (for example, a function which existed in an older version of
    * the codebase but was deleted).
    *
    * These symbols had PHIDs assigned to them, and may have other sorts of
    * metadata that we don't want to lose (like comments or flags), so we don't
    * delete them outright. They might also come back in the future: the change
    * which deleted the symbol might be reverted, or the documentation might
    * have been generated incorrectly by accident. In these cases, we can
    * restore the original data.
    *
    * However, most callers are not interested in these symbols, so they are
    * excluded by default. You can use this method to include them in results.
    *
    * @param bool  True to include ghosts.
    * @return this
    */
   public function withIncludeGhosts($include) {
     $this->includeGhosts = $include;
     return $this;
   }
 
 
   public function needExtends($need) {
     $this->needExtends = $need;
     return $this;
   }
 
   public function withIncludeUndocumentable($include) {
     $this->includeUndocumentable = $include;
     return $this;
   }
 
   protected function loadPage() {
     $table = new DivinerLiveSymbol();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     return $table->loadAllFromArray($data);
   }
 
   protected function willFilterPage(array $atoms) {
     $books = array_unique(mpull($atoms, 'getBookPHID'));
 
     $books = id(new DivinerBookQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($books)
       ->execute();
     $books = mpull($books, null, 'getPHID');
 
     foreach ($atoms as $key => $atom) {
       $book = idx($books, $atom->getBookPHID());
       if (!$book) {
         unset($atoms[$key]);
         continue;
       }
       $atom->attachBook($book);
     }
 
     if ($this->needAtoms) {
       $atom_data = id(new DivinerLiveAtom())->loadAllWhere(
         'symbolPHID IN (%Ls)',
         mpull($atoms, 'getPHID'));
       $atom_data = mpull($atom_data, null, 'getSymbolPHID');
 
       foreach ($atoms as $key => $atom) {
         $data = idx($atom_data, $atom->getPHID());
         if (!$data) {
           unset($atoms[$key]);
           continue;
         }
         $atom->attachAtom($data);
       }
     }
 
     // Load all of the symbols this symbol extends, recursively. Commonly,
     // this means all the ancestor classes and interfaces it extends and
     // implements.
 
     if ($this->needExtends) {
 
       // First, load all the matching symbols by name. This does 99% of the
       // work in most cases, assuming things are named at all reasonably.
 
       $names = array();
       foreach ($atoms as $atom) {
         foreach ($atom->getAtom()->getExtends() as $xref) {
           $names[] = $xref->getName();
         }
       }
 
       if ($names) {
         $xatoms = id(new DivinerAtomQuery())
           ->setViewer($this->getViewer())
           ->withNames($names)
           ->needExtends(true)
           ->needAtoms(true)
           ->needChildren($this->needChildren)
           ->execute();
         $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID');
       } else {
         $xatoms = array();
       }
 
       foreach ($atoms as $atom) {
         $alang = $atom->getAtom()->getLanguage();
         $extends = array();
         foreach ($atom->getAtom()->getExtends() as $xref) {
 
           // If there are no symbols of the matching name and type, we can't
           // resolve this.
           if (empty($xatoms[$xref->getName()][$xref->getType()])) {
             continue;
           }
 
           // If we found matches in the same documentation book, prefer them
           // over other matches. Otherwise, look at all the the matches.
           $matches = $xatoms[$xref->getName()][$xref->getType()];
           if (isset($matches[$atom->getBookPHID()])) {
             $maybe = $matches[$atom->getBookPHID()];
           } else {
             $maybe = array_mergev($matches);
           }
 
           if (!$maybe) {
             continue;
           }
 
           // Filter out matches in a different language, since, e.g., PHP
           // classes can not implement JS classes.
           $same_lang = array();
           foreach ($maybe as $xatom) {
             if ($xatom->getAtom()->getLanguage() == $alang) {
               $same_lang[] = $xatom;
             }
           }
 
           if (!$same_lang) {
             continue;
           }
 
           // If we have duplicates remaining, just pick the first one. There's
           // nothing more we can do to figure out which is the real one.
           $extends[] = head($same_lang);
         }
 
         $atom->attachExtends($extends);
       }
     }
 
     if ($this->needChildren) {
       $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends);
 
       if ($child_hashes) {
         $children = id(new DivinerAtomQuery())
           ->setViewer($this->getViewer())
           ->withIncludeUndocumentable(true)
           ->withNodeHashes($child_hashes)
           ->needAtoms($this->needAtoms)
           ->execute();
 
         $children = mpull($children, null, 'getNodeHash');
       } else {
         $children = array();
       }
 
       $this->attachAllChildren($atoms, $children, $this->needExtends);
     }
 
     return $atoms;
   }
 
   private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids) {
       $where[] = qsprintf(
         $conn_r,
         'phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->bookPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'bookPHID IN (%Ls)',
         $this->bookPHIDs);
     }
 
     if ($this->types) {
       $where[] = qsprintf(
         $conn_r,
         'type IN (%Ls)',
         $this->types);
     }
 
     if ($this->names) {
       $where[] = qsprintf(
         $conn_r,
         'name IN (%Ls)',
         $this->names);
     }
 
     if ($this->titles) {
       $hashes = array();
       foreach ($this->titles as $title) {
         $slug = DivinerAtomRef::normalizeTitleString($title);
         $hash = PhabricatorHash::digestForIndex($slug);
         $hashes[] = $hash;
       }
 
       $where[] = qsprintf(
         $conn_r,
         'titleSlugHash in (%Ls)',
         $hashes);
     }
 
     if ($this->contexts) {
       $with_null = false;
       $contexts = $this->contexts;
       foreach ($contexts as $key => $value) {
         if ($value === null) {
           unset($contexts[$key]);
           $with_null = true;
           continue;
         }
       }
 
       if ($contexts && $with_null) {
         $where[] = qsprintf(
           $conn_r,
           'context IN (%Ls) OR context IS NULL',
           $contexts);
       } else if ($contexts) {
         $where[] = qsprintf(
           $conn_r,
           'context IN (%Ls)',
           $contexts);
       } else if ($with_null) {
         $where[] = qsprintf(
           $conn_r,
           'context IS NULL');
       }
     }
 
     if ($this->indexes) {
       $where[] = qsprintf(
         $conn_r,
         'atomIndex IN (%Ld)',
         $this->indexes);
     }
 
     if (!$this->includeUndocumentable) {
       $where[] = qsprintf(
         $conn_r,
         'isDocumentable = 1');
     }
 
     if (!$this->includeGhosts) {
       $where[] = qsprintf(
         $conn_r,
         'graphHash IS NOT NULL');
     }
 
     if ($this->nodeHashes) {
       $where[] = qsprintf(
         $conn_r,
         'nodeHash IN (%Ls)',
         $this->nodeHashes);
     }
 
+    if ($this->nameContains) {
+      // NOTE: This CONVERT() call makes queries case-insensitive, since the
+      // column has binary collation. Eventually, this should move into
+      // fulltext.
+
+      $where[] = qsprintf(
+        $conn_r,
+        'CONVERT(name USING utf8) LIKE %~',
+        $this->nameContains);
+    }
+
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
 
   /**
    * Walk a list of atoms and collect all the node hashes of the atoms'
    * children. When recursing, also walk up the tree and collect children of
    * atoms they extend.
    *
    * @param list<DivinerLiveSymbol> List of symbols to collect child hashes of.
    * @param bool                    True to collect children of extended atoms,
    *                                as well.
    * @return map<string, string>    Hashes of atoms' children.
    */
   private function getAllChildHashes(array $symbols, $recurse_up) {
     assert_instances_of($symbols, 'DivinerLiveSymbol');
 
     $hashes = array();
     foreach ($symbols as $symbol) {
       foreach ($symbol->getAtom()->getChildHashes() as $hash) {
         $hashes[$hash] = $hash;
       }
       if ($recurse_up) {
         $hashes += $this->getAllChildHashes($symbol->getExtends(), true);
       }
     }
 
     return $hashes;
   }
 
 
   /**
    * Attach child atoms to existing atoms. In recursive mode, also attach child
    * atoms to atoms that these atoms extend.
    *
    * @param list<DivinerLiveSymbol> List of symbols to attach childeren to.
    * @param map<string, DivinerLiveSymbol> Map of symbols, keyed by node hash.
    * @param bool True to attach children to extended atoms, as well.
    * @return void
    */
   private function attachAllChildren(
     array $symbols,
     array $children,
     $recurse_up) {
 
     assert_instances_of($symbols, 'DivinerLiveSymbol');
     assert_instances_of($children, 'DivinerLiveSymbol');
 
     foreach ($symbols as $symbol) {
       $symbol_children = array();
       foreach ($symbol->getAtom()->getChildHashes() as $hash) {
         if (isset($children[$hash])) {
           $symbol_children[] = $children[$hash];
         }
       }
       $symbol->attachChildren($symbol_children);
       if ($recurse_up) {
         $this->attachAllChildren($symbol->getExtends(), $children, true);
       }
     }
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorApplicationDiviner';
   }
 
 }
diff --git a/src/applications/diviner/query/DivinerAtomSearchEngine.php b/src/applications/diviner/query/DivinerAtomSearchEngine.php
index 2701e2643..6c8382be3 100644
--- a/src/applications/diviner/query/DivinerAtomSearchEngine.php
+++ b/src/applications/diviner/query/DivinerAtomSearchEngine.php
@@ -1,49 +1,91 @@
 <?php
 
 final class DivinerAtomSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
 
+    $saved->setParameter(
+      'types',
+      $this->readListFromRequest($request, 'types'));
+
+    $saved->setParameter('name', $request->getStr('name'));
+
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new DivinerAtomQuery());
 
+    $types = $saved->getParameter('types');
+    if ($types) {
+      $query->withTypes($types);
+    }
+
+    $name = $saved->getParameter('name');
+    if ($name) {
+      $query->withNameContains($name);
+    }
+
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
-    PhabricatorSavedQuery $saved_query) {
+    PhabricatorSavedQuery $saved) {
+
+    $all_types = array();
+    foreach (DivinerAtom::getAllTypes() as $type) {
+      $all_types[$type] = DivinerAtom::getAtomTypeNameString($type);
+    }
+    asort($all_types);
+
+    $types = $saved->getParameter('types', array());
+    $types = array_fuse($types);
+    $type_control = id(new AphrontFormCheckboxControl())
+      ->setLabel(pht('Types'));
+    foreach ($all_types as $type => $name) {
+      $type_control->addCheckbox(
+        'types[]',
+        $type,
+        $name,
+        isset($types[$type]));
+    }
+
+    $form
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setLabel(pht('Name Contains'))
+          ->setName('name')
+          ->setValue($saved->getParameter('name')))
+      ->appendChild($type_control);
 
   }
 
   protected function getURI($path) {
     return '/diviner/'.$path;
   }
 
   public function getBuiltinQueryNames() {
     $names = array(
       'all' => pht('All'),
     );
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
 
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'all':
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
 }