summaryrefslogtreecommitdiff
path: root/modules/search
diff options
context:
space:
mode:
authorTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
committerTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
commit406abd7c4df1ace2cd3e4e17159e8941a2e8c0c4 (patch)
treea324be16021f44f2fd6d55e609f47024e945b1db /modules/search
Initial import
Diffstat (limited to 'modules/search')
-rw-r--r--modules/search/controllers/search.php123
-rw-r--r--modules/search/helpers/search.php163
-rw-r--r--modules/search/helpers/search_event.php39
-rw-r--r--modules/search/helpers/search_installer.php50
-rw-r--r--modules/search/helpers/search_task.php84
-rw-r--r--modules/search/helpers/search_theme.php29
-rw-r--r--modules/search/models/search_record.php24
-rw-r--r--modules/search/module.info7
-rw-r--r--modules/search/views/search.html.php65
-rw-r--r--modules/search/views/search_link.html.php22
10 files changed, 606 insertions, 0 deletions
diff --git a/modules/search/controllers/search.php b/modules/search/controllers/search.php
new file mode 100644
index 0000000..753d9b6
--- /dev/null
+++ b/modules/search/controllers/search.php
@@ -0,0 +1,123 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Search_Controller extends Controller {
+ public function index() {
+ $page_size = module::get_var("gallery", "page_size", 9);
+ $q = Input::instance()->get("q");
+ $q_with_more_terms = search::add_query_terms($q);
+ $show = Input::instance()->get("show");
+
+ $album_id = Input::instance()->get("album", item::root()->id);
+ $album = ORM::factory("item", $album_id);
+ if (!access::can("view", $album) || !$album->is_album()) {
+ $album = item::root();
+ }
+
+ if ($show) {
+ $child = ORM::factory("item", $show);
+ $index = search::get_position_within_album($child, $q_with_more_terms, $album);
+ if ($index) {
+ $page = ceil($index / $page_size);
+ url::redirect(url::abs_site("search" .
+ "?q=" . urlencode($q) .
+ "&album=" . urlencode($album->id) .
+ ($page == 1 ? "" : "&page=$page")));
+ }
+ }
+
+ $page = Input::instance()->get("page", 1);
+
+ // Make sure that the page references a valid offset
+ if ($page < 1) {
+ $page = 1;
+ }
+
+ $offset = ($page - 1) * $page_size;
+
+ list ($count, $result) =
+ search::search_within_album($q_with_more_terms, $album, $page_size, $offset);
+
+ $max_pages = max(ceil($count / $page_size), 1);
+
+ $template = new Theme_View("page.html", "collection", "search");
+ $root = item::root();
+ $template->set_global(
+ array("page" => $page,
+ "max_pages" => $max_pages,
+ "page_size" => $page_size,
+ "breadcrumbs" => array(
+ Breadcrumb::instance($root->title, $root->url())->set_first(),
+ Breadcrumb::instance($q, url::abs_site("search?q=" . urlencode($q)))->set_last(),
+ ),
+ "children_count" => $count));
+
+ $template->content = new View("search.html");
+ $template->content->album = $album;
+ $template->content->items = $result;
+ $template->content->q = $q;
+
+ print $template;
+
+ item::set_display_context_callback("Search_Controller::get_display_context", $album, $q);
+ }
+
+ static function get_display_context($item, $album, $q) {
+ $q_with_more_terms = search::add_query_terms($q);
+ $position = search::get_position_within_album($item, $q_with_more_terms, $album);
+
+ if ($position > 1) {
+ list ($count, $result_data) =
+ search::search_within_album($q_with_more_terms, $album, 3, $position - 2);
+ list ($previous_item, $ignore, $next_item) = $result_data;
+ } else {
+ $previous_item = null;
+ list ($count, $result_data) =
+ search::search_within_album($q_with_more_terms, $album, 1, $position);
+ list ($next_item) = $result_data;
+ }
+
+ $search_url = url::abs_site("search" .
+ "?q=" . urlencode($q) .
+ "&album=" . urlencode($album->id) .
+ "&show={$item->id}");
+ $root = item::root();
+
+ return array("position" => $position,
+ "previous_item" => $previous_item,
+ "next_item" => $next_item,
+ "sibling_count" => $count,
+ "siblings_callback" => array("Search_Controller::get_siblings", array($q, $album)),
+ "breadcrumbs" => array(
+ Breadcrumb::instance($root->title, $root->url())->set_first(),
+ Breadcrumb::instance(t("Search: %q", array("q" => $q)), $search_url),
+ Breadcrumb::instance($item->title, $item->url())->set_last()));
+ }
+
+ static function get_siblings($q, $album, $limit, $offset) {
+ if (!isset($limit)) {
+ $limit = 100;
+ }
+ if (!isset($offset)) {
+ $offset = 1;
+ }
+ $result = search::search_within_album(search::add_query_terms($q), $album, $limit, $offset);
+ return $result[1];
+ }
+}
diff --git a/modules/search/helpers/search.php b/modules/search/helpers/search.php
new file mode 100644
index 0000000..c84b70b
--- /dev/null
+++ b/modules/search/helpers/search.php
@@ -0,0 +1,163 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class search_Core {
+ /**
+ * Add more terms to the query by wildcarding the stem value of the first
+ * few terms in the query.
+ */
+ static function add_query_terms($q) {
+ $MAX_TERMS = 5;
+ $terms = explode(" ", $q, $MAX_TERMS);
+ for ($i = 0; $i < min(count($terms), $MAX_TERMS - 1); $i++) {
+ // Don't wildcard quoted or already wildcarded terms
+ if ((substr($terms[$i], 0, 1) != '"') && (substr($terms[$i], -1, 1) != "*")) {
+ $terms[] = rtrim($terms[$i], "s") . "*";
+ }
+ }
+ return implode(" ", $terms);
+ }
+
+ static function search($q, $limit, $offset) {
+ return search::search_within_album($q, item::root(), $limit, $offset);
+ }
+
+ static function search_within_album($q, $album, $limit, $offset) {
+ $db = Database::instance();
+
+ $query = self::_build_query_base($q, $album) .
+ " ORDER BY `score` DESC" .
+ " LIMIT $limit OFFSET " . (int)$offset;
+
+ $data = $db->query($query);
+ $count = $db->query("SELECT FOUND_ROWS() as c")->current()->c;
+
+ return array($count, new ORM_Iterator(ORM::factory("item"), $data));
+ }
+
+ private static function _build_query_base($q, $album, $where=array()) {
+ $db = Database::instance();
+ $q = $db->escape($q);
+
+ if (!identity::active_user()->admin) {
+ foreach (identity::group_ids_for_active_user() as $id) {
+ $fields[] = "`view_$id` = TRUE"; // access::ALLOW
+ }
+ $access_sql = " AND (" . join(" OR ", $fields) . ")";
+ } else {
+ $access_sql = "";
+ }
+
+ if ($album->id == item::root()->id) {
+ $album_sql = "";
+ } else {
+ $album_sql =
+ " AND {items}.left_ptr > " . $db->escape($album->left_ptr) .
+ " AND {items}.right_ptr <= " . $db->escape($album->right_ptr);
+ }
+
+ return
+ "SELECT SQL_CALC_FOUND_ROWS {items}.*, " .
+ " MATCH({search_records}.`data`) AGAINST ('$q') AS `score` " .
+ "FROM {items} JOIN {search_records} ON ({items}.`id` = {search_records}.`item_id`) " .
+ "WHERE MATCH({search_records}.`data`) AGAINST ('$q' IN BOOLEAN MODE)" .
+ $album_sql .
+ (empty($where) ? "" : " AND " . join(" AND ", $where)) .
+ $access_sql;
+ }
+
+ /**
+ * @return string An error message suitable for inclusion in the task log
+ */
+ static function check_index() {
+ list ($remaining) = search::stats();
+ if ($remaining) {
+ site_status::warning(
+ t('Your search index needs to be updated. <a href="%url" class="g-dialog-link">Fix this now</a>',
+ array("url" => html::mark_clean(url::site("admin/maintenance/start/search_task::update_index?csrf=__CSRF__")))),
+ "search_index_out_of_date");
+ }
+ }
+
+ static function update($item) {
+ $data = new ArrayObject();
+ $record = ORM::factory("search_record")->where("item_id", "=", $item->id)->find();
+ if (!$record->loaded()) {
+ $record->item_id = $item->id;
+ }
+
+ $item = $record->item();
+ module::event("item_index_data", $item, $data);
+ $record->data = join(" ", (array)$data);
+ $record->dirty = 0;
+ $record->save();
+ }
+
+ static function stats() {
+ $remaining = db::build()
+ ->from("items")
+ ->join("search_records", "items.id", "search_records.item_id", "left")
+ ->and_open()
+ ->where("search_records.item_id", "IS", null)
+ ->or_where("search_records.dirty", "=", 1)
+ ->close()
+ ->count_records();
+
+ $total = ORM::factory("item")->count_all();
+ $percent = round(100 * ($total - $remaining) / $total);
+
+ return array($remaining, $total, $percent);
+ }
+
+ static function get_position($item, $q) {
+ return search::get_position_within_album($item, $q, item::root());
+ }
+
+ static function get_position_within_album($item, $q, $album) {
+ $page_size = module::get_var("gallery", "page_size", 9);
+ $query = self::_build_query_base($q, $album, array("{items}.id = " . $item->id)) .
+ " ORDER BY `score` DESC";
+ $db = Database::instance();
+
+ // Truncate the score by two decimal places as this resolves the issues
+ // that arise due to inexact numeric conversions.
+ $current = $db->query($query)->current();
+ if (!$current) {
+ // We can't find this result in our result set - perhaps we've fallen out of context? Clear
+ // the context and try again.
+ item::clear_display_context_callback();
+ url::redirect(url::current());
+ }
+ $score = $current->score;
+ if (strlen($score) > 7) {
+ $score = substr($score, 0, strlen($score) - 2);
+ }
+
+ // Redo the query but only look for results greater than or equal to our current location
+ // then seek backwards until we find our item.
+ $data = $db->query(self::_build_query_base($q, $album) . " HAVING `score` >= " . $score .
+ " ORDER BY `score` DESC");
+ $data->seek($data->count() - 1);
+
+ while ($data->get("id") != $item->id && $data->prev()->valid()) {
+ }
+
+ return $data->key() + 1;
+ }
+}
diff --git a/modules/search/helpers/search_event.php b/modules/search/helpers/search_event.php
new file mode 100644
index 0000000..a20935b
--- /dev/null
+++ b/modules/search/helpers/search_event.php
@@ -0,0 +1,39 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class search_event_Core {
+ static function item_created($item) {
+ search::update($item);
+ }
+
+ static function item_updated($original, $new) {
+ search::update($new);
+ }
+
+ static function item_deleted($item) {
+ db::build()
+ ->delete("search_records")
+ ->where("item_id", "=", $item->id)
+ ->execute();
+ }
+
+ static function item_related_update($item) {
+ search::update($item);
+ }
+}
diff --git a/modules/search/helpers/search_installer.php b/modules/search/helpers/search_installer.php
new file mode 100644
index 0000000..c9e8f26
--- /dev/null
+++ b/modules/search/helpers/search_installer.php
@@ -0,0 +1,50 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class search_installer {
+ static function install() {
+ $db = Database::instance();
+ $db->query("CREATE TABLE {search_records} (
+ `id` int(9) NOT NULL auto_increment,
+ `item_id` int(9),
+ `dirty` boolean default 1,
+ `data` LONGTEXT default NULL,
+ PRIMARY KEY (`id`),
+ KEY(`item_id`),
+ FULLTEXT INDEX (`data`))
+ ENGINE=MyISAM
+ DEFAULT CHARSET=utf8;");
+ }
+
+ static function activate() {
+ // Update the root item. This is a quick hack because the search module is activated as part
+ // of the official install, so this way we don't start off with a "your index is out of date"
+ // banner.
+ search::update(model_cache::get("item", 1));
+ search::check_index();
+ }
+
+ static function deactivate() {
+ site_status::clear("search_index_out_of_date");
+ }
+
+ static function uninstall() {
+ Database::instance()->query("DROP TABLE {search_records}");
+ }
+}
diff --git a/modules/search/helpers/search_task.php b/modules/search/helpers/search_task.php
new file mode 100644
index 0000000..18348a2
--- /dev/null
+++ b/modules/search/helpers/search_task.php
@@ -0,0 +1,84 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class search_task_Core {
+ static function available_tasks() {
+ // Delete extra search_records
+ db::build()
+ ->delete("search_records")
+ ->where("item_id", "NOT IN", db::build()->select("id")->from("items"))
+ ->execute();
+
+ list ($remaining, $total, $percent) = search::stats();
+ return array(Task_Definition::factory()
+ ->callback("search_task::update_index")
+ ->name(t("Update Search Index"))
+ ->description(
+ $remaining
+ ? t2("1 photo or album needs to be scanned",
+ "%count (%percent%) of your photos and albums need to be scanned",
+ $remaining, array("percent" => (100 - $percent)))
+ : t("Search data is up-to-date"))
+ ->severity($remaining ? log::WARNING : log::SUCCESS));
+ }
+
+ static function update_index($task) {
+ try {
+ $completed = $task->get("completed", 0);
+
+ $start = microtime(true);
+ foreach (ORM::factory("item")
+ ->join("search_records", "items.id", "search_records.item_id", "left")
+ ->where("search_records.item_id", "IS", null)
+ ->or_where("search_records.dirty", "=", 1)
+ ->find_all(100) as $item) {
+ // The query above can take a long time, so start the timer after its done
+ // to give ourselves a little time to actually process rows.
+ if (!isset($start)) {
+ $start = microtime(true);
+ }
+
+ search::update($item);
+ $completed++;
+
+ if (microtime(true) - $start > .75) {
+ break;
+ }
+ }
+
+ list ($remaining, $total, $percent) = search::stats();
+ $task->set("completed", $completed);
+ if ($remaining == 0 || !($remaining + $completed)) {
+ $task->done = true;
+ $task->state = "success";
+ site_status::clear("search_index_out_of_date");
+ $task->percent_complete = 100;
+ } else {
+ $task->percent_complete = round(100 * $completed / ($remaining + $completed));
+ }
+ $task->status = t2("one record updated, index is %percent% up-to-date",
+ "%count records updated, index is %percent% up-to-date",
+ $completed, array("percent" => $percent));
+ } catch (Exception $e) {
+ $task->done = true;
+ $task->state = "error";
+ $task->status = $e->getMessage();
+ }
+ }
+}
diff --git a/modules/search/helpers/search_theme.php b/modules/search/helpers/search_theme.php
new file mode 100644
index 0000000..e8735d1
--- /dev/null
+++ b/modules/search/helpers/search_theme.php
@@ -0,0 +1,29 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class search_theme_Core {
+ static function header_top($theme) {
+ if ($theme->page_subtype() != "login") {
+ $view = new View("search_link.html");
+ return $view->render();
+ } else {
+ return "";
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/search/models/search_record.php b/modules/search/models/search_record.php
new file mode 100644
index 0000000..be68e72
--- /dev/null
+++ b/modules/search/models/search_record.php
@@ -0,0 +1,24 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Search_Record_Model_Core extends ORM {
+ function item() {
+ return model_cache::get("item", $this->item_id);
+ }
+}
diff --git a/modules/search/module.info b/modules/search/module.info
new file mode 100644
index 0000000..f1bb1fa
--- /dev/null
+++ b/modules/search/module.info
@@ -0,0 +1,7 @@
+name = "Search"
+description = "Allows users to search their Gallery"
+version = 1
+author_name = "Gallery Team"
+author_url = "http://codex.galleryproject.org/Gallery:Team"
+info_url = "http://codex.galleryproject.org/Gallery3:Modules:search"
+discuss_url = "http://galleryproject.org/forum_module_search"
diff --git a/modules/search/views/search.html.php b/modules/search/views/search.html.php
new file mode 100644
index 0000000..a42c31d
--- /dev/null
+++ b/modules/search/views/search.html.php
@@ -0,0 +1,65 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<? // @todo Set hover on AlbumGrid list items ?>
+<form action="<?= url::site("/search") ?>" id="g-search-form" class="g-short-form">
+ <fieldset>
+ <legend>
+ <?= t("Search") ?>
+ </legend>
+ <ul>
+ <li>
+ <? if ($album->id == item::root()->id): ?>
+ <label for="q"><?= t("Search the gallery") ?></label>
+ <? else: ?>
+ <label for="q"><?= t("Search this album") ?></label>
+ <? endif; ?>
+ <input name="album" type="hidden" value="<?= html::clean_attribute($album->id) ?>" />
+ <input name="q" id="q" type="text" value="<?= html::clean_attribute($q) ?>" class="text" />
+ </li>
+ <li>
+ <input type="submit" value="<?= t("Search")->for_html_attr() ?>" class="submit" />
+ </li>
+ </ul>
+ </fieldset>
+</form>
+
+<div id="g-search-results">
+ <h1><?= t("Search results") ?></h1>
+
+ <? if ($album->id == item::root()->id): ?>
+ <div>
+ <?= t("Searched the whole gallery.") ?>
+ </div>
+ <? else: ?>
+ <div>
+ <?= t("Searched within album <b>%album</b>.", array("album" => html::purify($album->title))) ?>
+ <a href="<?= url::site(url::merge(array("album" => item::root()->id))) ?>"><?= t("Search whole gallery") ?></a>
+ </div>
+ <? endif; ?>
+
+ <? if (count($items)): ?>
+ <ul id="g-album-grid" class="ui-helper-clearfix">
+ <? foreach ($items as $item): ?>
+ <? $item_class = $item->is_album() ? "g-album" : "g-photo" ?>
+ <li class="g-item <?= $item_class ?>">
+ <a href="<?= $item->url() ?>">
+ <?= $item->thumb_img(array("class" => "g-thumbnail")) ?>
+ <p>
+ <span class="<?= $item_class ?>"></span>
+ <?= html::purify(text::limit_chars($item->title, 32, "…")) ?>
+ </p>
+ <div>
+ <?= nl2br(html::purify(text::limit_chars($item->description, 64, "…"))) ?>
+ </div>
+ </a>
+ </li>
+ <? endforeach ?>
+ </ul>
+ <?= $theme->paginator() ?>
+
+ <? else: ?>
+ <p>
+ <?= t("No results found for <b>%term</b>", array("term" => $q)) ?>
+ </p>
+
+ <? endif; ?>
+</div>
diff --git a/modules/search/views/search_link.html.php b/modules/search/views/search_link.html.php
new file mode 100644
index 0000000..4f9abc1
--- /dev/null
+++ b/modules/search/views/search_link.html.php
@@ -0,0 +1,22 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<form action="<?= url::site("search") ?>" id="g-quick-search-form" class="g-short-form">
+ <? if (isset($item)): ?>
+ <? $album_id = $item->is_album() ? $item->id : $item->parent_id; ?>
+ <? else: ?>
+ <? $album_id = item::root()->id; ?>
+ <? endif; ?>
+ <ul>
+ <li>
+ <? if ($album_id == item::root()->id): ?>
+ <label for="g-search"><?= t("Search the gallery") ?></label>
+ <? else: ?>
+ <label for="g-search"><?= t("Search this album") ?></label>
+ <? endif; ?>
+ <input type="hidden" name="album" value="<?= $album_id ?>" />
+ <input type="text" name="q" id="g-search" class="text" />
+ </li>
+ <li>
+ <input type="submit" value="<?= t("Go")->for_html_attr() ?>" class="submit" />
+ </li>
+ </ul>
+</form>