diff --git a/inc/Action/Search.php b/inc/Action/Search.php
index d4833f4539c7e828fc4b79003e596cb7a6a2bdde..1fa19d88914bb30e39a525004a37b07934fc2edb 100644
--- a/inc/Action/Search.php
+++ b/inc/Action/Search.php
@@ -32,6 +32,9 @@ class Search extends AbstractAction {
 
     /** @inheritdoc */
     public function tplContent() {
-        html_search();
+        global $QUERY;
+        $search = new \dokuwiki\Ui\Search($QUERY);
+        $search->execute();
+        $search->show();
     }
 }
diff --git a/inc/Ui/Search.php b/inc/Ui/Search.php
new file mode 100644
index 0000000000000000000000000000000000000000..da030d5a2dab8f8138a9f0bad3b73922523177e4
--- /dev/null
+++ b/inc/Ui/Search.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace dokuwiki\Ui;
+
+class Search extends Ui
+{
+    protected $query;
+    protected $pageLookupResults = array();
+    protected $fullTextResults = array();
+    protected $highlight = array();
+
+    /**
+     * Search constructor.
+     *
+     * @param string $query the search query
+     */
+    public function __construct($query)
+    {
+        $this->query = $query;
+    }
+
+    /**
+     * run the search
+     */
+    public function execute()
+    {
+        $this->pageLookupResults = ft_pageLookup($this->query, true, useHeading('navigation'));
+        $this->fullTextResults = ft_pageSearch($this->query, $highlight);
+        $this->highlight = $highlight;
+    }
+
+    /**
+     * display the search result
+     *
+     * @return void
+     */
+    public function show()
+    {
+        $searchHTML = '';
+
+        $searchHTML .= $this->getSearchIntroHTML($this->query);
+
+        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
+
+        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
+
+        echo $searchHTML;
+    }
+
+    /**
+     * Build the intro text for the search page
+     *
+     * @param string $query the search query
+     *
+     * @return string
+     */
+    protected function getSearchIntroHTML($query)
+    {
+        global $ID, $lang;
+
+        $intro = p_locale_xhtml('searchpage');
+        // allow use of placeholder in search intro
+        $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
+        $intro = str_replace(
+            array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
+            array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
+            $intro
+        );
+        return $intro;
+    }
+
+
+    /**
+     * Build HTML for a list of pages with matching pagenames
+     *
+     * @param array $data search results
+     *
+     * @return string
+     */
+    protected function getPageLookupHTML($data)
+    {
+        if (empty($data)) {
+            return '';
+        }
+
+        global $lang;
+
+        $html = '<div class="search_quickresult">';
+        $html .= '<h3>' . $lang['quickhits'] . ':</h3>';
+        $html .= '<ul class="search_quickhits">';
+        foreach ($data as $id => $title) {
+            $html .= '<li> ';
+            if (useHeading('navigation')) {
+                $name = $title;
+            } else {
+                $ns = getNS($id);
+                if ($ns) {
+                    $name = shorten(noNS($id), ' (' . $ns . ')', 30);
+                } else {
+                    $name = $id;
+                }
+            }
+            $html .= html_wikilink(':' . $id, $name);
+            $html .= '</li> ';
+        }
+        $html .= '</ul> ';
+        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
+        $html .= '<div class="clearer"></div>';
+        $html .= '</div>';
+
+        return $html;
+    }
+
+    /**
+     * Build HTML for fulltext search results or "no results" message
+     *
+     * @param array $data      the results of the fulltext search
+     * @param array $highlight the terms to be highlighted in the results
+     *
+     * @return string
+     */
+    protected function getFulltextResultsHTML($data, $highlight)
+    {
+        global $lang;
+
+        if (empty($data)) {
+            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
+        }
+
+        $html = '';
+        $html .= '<dl class="search_results">';
+        $num = 1;
+        foreach ($data as $id => $cnt) {
+            $html .= '<dt>';
+            $html .= html_wikilink(':' . $id, useHeading('navigation') ? null : $id, $highlight);
+            if ($cnt !== 0) {
+                $html .= ': ' . $cnt . ' ' . $lang['hits'] . '';
+            }
+            $html .= '</dt>';
+            if ($cnt !== 0) {
+                if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
+                    $html .= '<dd>' . ft_snippet($id, $highlight) . '</dd>';
+                }
+                $num++;
+            }
+        }
+        $html .= '</dl>';
+
+        return $html;
+    }
+}
diff --git a/inc/html.php b/inc/html.php
index 7cf0452dbf49aec1d0dfcd4cfe7036554e9c5e07..973b1d1213f4ef0e20d4eb3bf219fa148d7d34b7 100644
--- a/inc/html.php
+++ b/inc/html.php
@@ -362,78 +362,6 @@ function html_hilight_callback($m) {
     return $hlight;
 }
 
-/**
- * Run a search and display the result
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- */
-function html_search(){
-    global $QUERY, $ID;
-    global $lang;
-
-    $intro = p_locale_xhtml('searchpage');
-    // allow use of placeholder in search intro
-    $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
-    $intro = str_replace(
-        array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
-        array(hsc(rawurlencode($QUERY)), hsc($QUERY), $pagecreateinfo),
-        $intro
-    );
-    echo $intro;
-
-    //do quick pagesearch
-    $data = ft_pageLookup($QUERY,true,useHeading('navigation'));
-    if(count($data)){
-        print '<div class="search_quickresult">';
-        print '<h3>'.$lang['quickhits'].':</h3>';
-        print '<ul class="search_quickhits">';
-        foreach($data as $id => $title){
-            print '<li> ';
-            if (useHeading('navigation')) {
-                $name = $title;
-            }else{
-                $ns = getNS($id);
-                if($ns){
-                    $name = shorten(noNS($id), ' ('.$ns.')',30);
-                }else{
-                    $name = $id;
-                }
-            }
-            print html_wikilink(':'.$id,$name);
-            print '</li> ';
-        }
-        print '</ul> ';
-        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
-        print '<div class="clearer"></div>';
-        print '</div>';
-    }
-
-    //do fulltext search
-    $regex = array();
-    $data = ft_pageSearch($QUERY,$regex);
-    if(count($data)){
-        print '<dl class="search_results">';
-        $num = 1;
-        foreach($data as $id => $cnt){
-            print '<dt>';
-            print html_wikilink(':'.$id,useHeading('navigation')?null:$id,$regex);
-            if($cnt !== 0){
-                print ': '.$cnt.' '.$lang['hits'].'';
-            }
-            print '</dt>';
-            if($cnt !== 0){
-                if($num < FT_SNIPPET_NUMBER){ // create snippets for the first number of matches only
-                    print '<dd>'.ft_snippet($id,$regex).'</dd>';
-                }
-                $num++;
-            }
-        }
-        print '</dl>';
-    }else{
-        print '<div class="nothing">'.$lang['nothingfound'].'</div>';
-    }
-}
-
 /**
  * Display error on locked pages
  *