<?php
// $Id: search.module,v 1.250.2.4 2008/09/17 06:42:20 goba Exp $
/**
* @file
* Enables site-wide keyword searching.
*/
/**
* Matches Unicode character classes to exclude from the search index.
*
* See: http://www.unicode.org/Public/UNIDATA/UCD.html#General_Category_Values
*
* The index only contains the following character classes:
* Lu Letter, Uppercase
* Ll Letter, Lowercase
* Lt Letter, Titlecase
* Lo Letter, Other
* Nd Number, Decimal Digit
* No Number, Other
*/
define('PREG_CLASS_SEARCH_EXCLUDE',
'\x{0}-\x{2f}\x{3a}-\x{40}\x{5b}-\x{60}\x{7b}-\x{bf}\x{d7}\x{f7}\x{2b0}-'.
'\x{385}\x{387}\x{3f6}\x{482}-\x{489}\x{559}-\x{55f}\x{589}-\x{5c7}\x{5f3}-'.
'\x{61f}\x{640}\x{64b}-\x{65e}\x{66a}-\x{66d}\x{670}\x{6d4}\x{6d6}-\x{6ed}'.
'\x{6fd}\x{6fe}\x{700}-\x{70f}\x{711}\x{730}-\x{74a}\x{7a6}-\x{7b0}\x{901}-'.
'\x{903}\x{93c}\x{93e}-\x{94d}\x{951}-\x{954}\x{962}-\x{965}\x{970}\x{981}-'.
'\x{983}\x{9bc}\x{9be}-\x{9cd}\x{9d7}\x{9e2}\x{9e3}\x{9f2}-\x{a03}\x{a3c}-'.
'\x{a4d}\x{a70}\x{a71}\x{a81}-\x{a83}\x{abc}\x{abe}-\x{acd}\x{ae2}\x{ae3}'.
'\x{af1}-\x{b03}\x{b3c}\x{b3e}-\x{b57}\x{b70}\x{b82}\x{bbe}-\x{bd7}\x{bf0}-'.
'\x{c03}\x{c3e}-\x{c56}\x{c82}\x{c83}\x{cbc}\x{cbe}-\x{cd6}\x{d02}\x{d03}'.
'\x{d3e}-\x{d57}\x{d82}\x{d83}\x{dca}-\x{df4}\x{e31}\x{e34}-\x{e3f}\x{e46}-'.
'\x{e4f}\x{e5a}\x{e5b}\x{eb1}\x{eb4}-\x{ebc}\x{ec6}-\x{ecd}\x{f01}-\x{f1f}'.
'\x{f2a}-\x{f3f}\x{f71}-\x{f87}\x{f90}-\x{fd1}\x{102c}-\x{1039}\x{104a}-'.
'\x{104f}\x{1056}-\x{1059}\x{10fb}\x{10fc}\x{135f}-\x{137c}\x{1390}-\x{1399}'.
'\x{166d}\x{166e}\x{1680}\x{169b}\x{169c}\x{16eb}-\x{16f0}\x{1712}-\x{1714}'.
'\x{1732}-\x{1736}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17db}\x{17dd}'.
'\x{17f0}-\x{180e}\x{1843}\x{18a9}\x{1920}-\x{1945}\x{19b0}-\x{19c0}\x{19c8}'.
'\x{19c9}\x{19de}-\x{19ff}\x{1a17}-\x{1a1f}\x{1d2c}-\x{1d61}\x{1d78}\x{1d9b}-'.
'\x{1dc3}\x{1fbd}\x{1fbf}-\x{1fc1}\x{1fcd}-\x{1fcf}\x{1fdd}-\x{1fdf}\x{1fed}-'.
'\x{1fef}\x{1ffd}-\x{2070}\x{2074}-\x{207e}\x{2080}-\x{2101}\x{2103}-\x{2106}'.
'\x{2108}\x{2109}\x{2114}\x{2116}-\x{2118}\x{211e}-\x{2123}\x{2125}\x{2127}'.
'\x{2129}\x{212e}\x{2132}\x{213a}\x{213b}\x{2140}-\x{2144}\x{214a}-\x{2b13}'.
'\x{2ce5}-\x{2cff}\x{2d6f}\x{2e00}-\x{3005}\x{3007}-\x{303b}\x{303d}-\x{303f}'.
'\x{3099}-\x{309e}\x{30a0}\x{30fb}-\x{30fe}\x{3190}-\x{319f}\x{31c0}-\x{31cf}'.
'\x{3200}-\x{33ff}\x{4dc0}-\x{4dff}\x{a015}\x{a490}-\x{a716}\x{a802}\x{a806}'.
'\x{a80b}\x{a823}-\x{a82b}\x{d800}-\x{f8ff}\x{fb1e}\x{fb29}\x{fd3e}\x{fd3f}'.
'\x{fdfc}-\x{fe6b}\x{feff}-\x{ff0f}\x{ff1a}-\x{ff20}\x{ff3b}-\x{ff40}\x{ff5b}-'.
'\x{ff65}\x{ff70}\x{ff9e}\x{ff9f}\x{ffe0}-\x{fffd}');
/**
* Matches all 'N' Unicode character classes (numbers)
*/
define('PREG_CLASS_NUMBERS',
'\x{30}-\x{39}\x{b2}\x{b3}\x{b9}\x{bc}-\x{be}\x{660}-\x{669}\x{6f0}-\x{6f9}'.
'\x{966}-\x{96f}\x{9e6}-\x{9ef}\x{9f4}-\x{9f9}\x{a66}-\x{a6f}\x{ae6}-\x{aef}'.
'\x{b66}-\x{b6f}\x{be7}-\x{bf2}\x{c66}-\x{c6f}\x{ce6}-\x{cef}\x{d66}-\x{d6f}'.
'\x{e50}-\x{e59}\x{ed0}-\x{ed9}\x{f20}-\x{f33}\x{1040}-\x{1049}\x{1369}-'.
'\x{137c}\x{16ee}-\x{16f0}\x{17e0}-\x{17e9}\x{17f0}-\x{17f9}\x{1810}-\x{1819}'.
'\x{1946}-\x{194f}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2153}-\x{2183}'.
'\x{2460}-\x{249b}\x{24ea}-\x{24ff}\x{2776}-\x{2793}\x{3007}\x{3021}-\x{3029}'.
'\x{3038}-\x{303a}\x{3192}-\x{3195}\x{3220}-\x{3229}\x{3251}-\x{325f}\x{3280}-'.
'\x{3289}\x{32b1}-\x{32bf}\x{ff10}-\x{ff19}');
/**
* Matches all 'P' Unicode character classes (punctuation)
*/
define('PREG_CLASS_PUNCTUATION',
'\x{21}-\x{23}\x{25}-\x{2a}\x{2c}-\x{2f}\x{3a}\x{3b}\x{3f}\x{40}\x{5b}-\x{5d}'.
'\x{5f}\x{7b}\x{7d}\x{a1}\x{ab}\x{b7}\x{bb}\x{bf}\x{37e}\x{387}\x{55a}-\x{55f}'.
'\x{589}\x{58a}\x{5be}\x{5c0}\x{5c3}\x{5f3}\x{5f4}\x{60c}\x{60d}\x{61b}\x{61f}'.
'\x{66a}-\x{66d}\x{6d4}\x{700}-\x{70d}\x{964}\x{965}\x{970}\x{df4}\x{e4f}'.
'\x{e5a}\x{e5b}\x{f04}-\x{f12}\x{f3a}-\x{f3d}\x{f85}\x{104a}-\x{104f}\x{10fb}'.
'\x{1361}-\x{1368}\x{166d}\x{166e}\x{169b}\x{169c}\x{16eb}-\x{16ed}\x{1735}'.
'\x{1736}\x{17d4}-\x{17d6}\x{17d8}-\x{17da}\x{1800}-\x{180a}\x{1944}\x{1945}'.
'\x{2010}-\x{2027}\x{2030}-\x{2043}\x{2045}-\x{2051}\x{2053}\x{2054}\x{2057}'.
'\x{207d}\x{207e}\x{208d}\x{208e}\x{2329}\x{232a}\x{23b4}-\x{23b6}\x{2768}-'.
'\x{2775}\x{27e6}-\x{27eb}\x{2983}-\x{2998}\x{29d8}-\x{29db}\x{29fc}\x{29fd}'.
'\x{3001}-\x{3003}\x{3008}-\x{3011}\x{3014}-\x{301f}\x{3030}\x{303d}\x{30a0}'.
'\x{30fb}\x{fd3e}\x{fd3f}\x{fe30}-\x{fe52}\x{fe54}-\x{fe61}\x{fe63}\x{fe68}'.
'\x{fe6a}\x{fe6b}\x{ff01}-\x{ff03}\x{ff05}-\x{ff0a}\x{ff0c}-\x{ff0f}\x{ff1a}'.
'\x{ff1b}\x{ff1f}\x{ff20}\x{ff3b}-\x{ff3d}\x{ff3f}\x{ff5b}\x{ff5d}\x{ff5f}-'.
'\x{ff65}');
/**
* Matches all CJK characters that are candidates for auto-splitting
* (Chinese, Japanese, Korean).
* Contains kana and BMP ideographs.
*/
define('PREG_CLASS_CJK', '\x{3041}-\x{30ff}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}'.
'\x{4e00}-\x{9fbb}\x{f900}-\x{fad9}');
/**
* Implementation of hook_help().
*/
function tsearch_help($path, $arg) {
switch ($path) {
case 'admin/help#search':
$output = '<p>'. t('The search module adds the ability to search for content by keywords. Search is often the only practical way to find content on a large site, and is useful for finding both users and posts.') .'</p>';
$output .= '<p>'. t('To provide keyword searching, the search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured <a href="@cron">cron maintenance task</a> is required. Indexing behavior can be adjusted using the <a href="@searchsettings">search settings page</a>; for example, the <em>Number of items to index per cron run</em> sets the maximum number of items indexed in each pass of a <a href="@cron">cron maintenance task</a>. If necessary, reduce this number to prevent timeouts and memory errors when indexing.', array('@cron' => url('admin/reports/status'), '@searchsettings' => url('admin/settings/search'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@search">Search module</a>.', array('@search' => 'http://drupal.org/handbook/modules/search/')) .'</p>';
return $output;
case 'admin/settings/search':
return '<p>'. t('The search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured <a href="@cron">cron maintenance task</a> is required. Indexing behavior can be adjusted using the settings below.', array('@cron' => url('admin/reports/status'))) .'</p>';
case 'search#noresults':
return t('<ul>
<li>Check if your spelling is correct.</li>
<li>Remove quotes around phrases to match each word individually: <em>"blue smurf"</em> will match less than <em>blue smurf</em>.</li>
<li>Consider loosening your query with <em>OR</em>: <em>blue smurf</em> will match less than <em>blue OR smurf</em>.</li>
</ul>');
}
}
/**
* Implementation of hook_theme()
*/
function tsearch_theme() {
return array(
'tsearch_theme_form' => array(
'arguments' => array('form' => NULL),
'template' => 'tsearch-theme-form',
),
'tsearch_block_form' => array(
'arguments' => array('form' => NULL),
'template' => 'tsearch-block-form',
),
'tsearch_result' => array(
'arguments' => array('result' => NULL, 'type' => NULL),
'file' => 'tsearch.pages.inc',
'template' => 'tsearch-result',
),
'tsearch_results' => array(
'arguments' => array('results' => NULL, 'type' => NULL),
'file' => 'tsearch.pages.inc',
'template' => 'tsearch-results',
),
);
}
/**
* Implementation of hook_perm().
*/
function tsearch_perm() {
return array('search content', 'use advanced search', 'administer search');
}
/**
* Implementation of hook_block().
*/
function tsearch_block($op = 'list', $delta = 0) {
if ($op == 'list') {
$blocks[0]['info'] = t('Search form');
// Not worth caching.
$blocks[0]['cache'] = BLOCK_NO_CACHE;
return $blocks;
}
else if ($op == 'view' && user_access('search content')) {
$block['content'] = drupal_get_form('tsearch_block_form');
$block['subject'] = t('Search');
return $block;
}
}
/**
* Implementation of hook_menu().
*/
function tsearch_menu() {
$items['tsearch'] = array(
'title' => 'Search',
'page callback' => 'tsearch_view',
'access arguments' => array('search content'),
'type' => MENU_SUGGESTED_ITEM,
'file' => 'tsearch.pages.inc',
);
$items['admin/settings/tsearch'] = array(
'title' => 'TSearch settings',
'description' => 'Configure relevance settings for search and other indexing options',
'page callback' => 'drupal_get_form',
'page arguments' => array('tsearch_admin_settings'),
'access arguments' => array('administer search'),
'type' => MENU_NORMAL_ITEM,
'file' => 'tsearch.admin.inc',
);
$items['admin/settings/tsearch/wipe'] = array(
'title' => 'Clear index',
'page callback' => 'drupal_get_form',
'page arguments' => array('tsearch_wipe_confirm'),
'access arguments' => array('administer search'),
'type' => MENU_CALLBACK,
'file' => 'tsearch.admin.inc',
);
$items['admin/reports/search'] = array(
'title' => 'Top search phrases',
'description' => 'View most popular search phrases.',
'page callback' => 'dblog_top',
'page arguments' => array('search'),
'access arguments' => array('access site reports'),
'file' => 'dblog.admin.inc',
'file path' => drupal_get_path('module', 'dblog'),
);
foreach (module_implements('search') as $name) {
$items['tsearch/'. $name .'/%menu_tail'] = array(
'title callback' => 'module_invoke',
'title arguments' => array($name, 'tsearch', 'name', TRUE),
'page callback' => 'tsearch_view',
'page arguments' => array($name),
'access callback' => '_tsearch_menu',
'access arguments' => array($name),
'type' => MENU_LOCAL_TASK,
'parent' => 'tsearch',
'file' => 'tsearch.pages.inc',
);
}
return $items;
}
function _tsearch_menu($name) {
return user_access('search content') && module_invoke($name, 'search', 'name');
}
/**
* Wipes a part of or the entire search index.
*
* @param $sid
* (optional) The SID of the item to wipe. If specified, $type must be passed
* too.
* @param $type
* (optional) The type of item to wipe.
*/
function tsearch_wipe($sid = NULL, $type = NULL, $reindex = FALSE) {
db_query("TRUNCATE TABLE tsearch_node");
}
/**
* Implementation of hook_cron().
*
* Select up to 100 nodes that need indexing and send them off for processing.
*/
function tsearch_cron() {
$update_nids = array();
$sql = <<<SQL
SELECT n.nid, t.updated
FROM node AS n
LEFT JOIN tsearch_node AS t ON (t.nid = n.nid)
WHERE ((
n.changed > t.updated
)
OR
(
t.updated IS NULL
)
)
LIMIT %d;
SQL;
$query = db_query($sql, variable_get('tsearch_cron_limit', 100));
while ($row = db_fetch_array($query)) {
// If updated is not null, we should update the index record instead of
// creating a new one, so update on tsearch_node should be passed TRUE
$update_nids[$row['nid']] = !is_null($row['updated']);
}
if (!empty($update_nids)) {
tsearch_index_nodes($update_nids);
}
}
/**
* Index a bunch of nodes
*
* @param array $update_nids
* An array of node IDs of the nodes that should be indexed
*/
function tsearch_index_nodes($update_nids) {
foreach ($update_nids as $nid => $update) {
tsearch_index_node($nid, $update);
}
}
/**
* Index a single node
*
* @param integer $nid
* The node ID of the node that should be indexed.
* @param boolean $update
* If the index record should be updated instead of created.
*/
function tsearch_index_node($nid, $update = NULL) {
// If $update is null, we don't know what to do, so let's find out.
if (is_null($update)) {
$query = db_query("SELECT nid FROM {tsearch_node} WHERE nid = %d", $nid);
$result = db_result($query);
if ($result && $result == $nid) {
$update = TRUE;
}
else {
$update = FALSE;
}
}
// Load our node
$node = node_load($nid);
// Build the node body.
$node->build_mode = NODE_BUILD_SEARCH_INDEX;
$node = node_build_content($node, FALSE, FALSE);
$node->body = drupal_render($node->content);
$text = array();
$text['title'] = check_plain($node->title);
$text['extra'] = '';
$text['teaser'] = tsearch_simplify($node->teaser);
$text['body'] = str_replace($text['teaser'], '', tsearch_simplify($node->body));
// Fetch extra data normally not visible
$extra = node_invoke_nodeapi($node, 'update index');
foreach ($extra as $t) {
$text['extra'] .= $t;
}
$lan = drupal_strtolower(tsearch_node_language_name($node));
// Now, let's stick it in the database
if ($update) {
db_query("UPDATE {tsearch_node} SET vid = %d,
node_tsvector =
setweight(to_tsvector('$lan', '%s'), 'A') ||
setweight(to_tsvector('$lan', '%s'), 'B') ||
setweight(to_tsvector('$lan', '%s'), 'C') ||
setweight(to_tsvector('$lan', '%s'), 'D'),
updated = %d
WHERE nid = %d",
array(':vid' => $node->vid,
':title' => $text['title'],
':extra' => $text['extra'],
':teaser' => $text['teaser'],
':body' => $text['body'],
':time' => $_SERVER['REQUEST_TIME'],
':nid' => $nid,
));
}
else {
db_query("INSERT INTO {tsearch_node} (nid, vid, node_tsvector, ts_language, updated)
VALUES (%d, %d,
setweight(to_tsvector('$lan', '%s'), 'A') ||
setweight(to_tsvector('$lan', '%s'), 'B') ||
setweight(to_tsvector('$lan', '%s'), 'C') ||
setweight(to_tsvector('$lan', '%s'), 'D'),
'%s', %d)",
array(':nid' => $nid,
':vid' => $node->vid,
':title' => $text['title'],
':extra' => $text['extra'],
':teaser' => $text['teaser'],
':body' => $text['body'],
':language' => $lan,
':time' => $_SERVER['REQUEST_TIME'],
));
}
}
/**
* Find the correct langugage name for a node
*
* @param $node
* The node we're working on.
* @return string
* The name of the language, in English.
*/
function tsearch_node_language_name($node) {
$result = '';
if (!empty($node->language) && function_exists('_locale_get_predefined_list')) {
$predef = _locale_get_predefined_list();
$result = $predef[$node->language][0];
// If the above didn't work, try loading the language with a more expensive call.
// Probably only used for custom languages.
if (empty($result)) {
$result = locale_language_name($node->language);
}
}
// Fall back to the default site language.
if (empty($result)) {
$result = $GLOBALS['language']->name;
}
// A last desparate measure if none of the above worked, default to English.
if (empty($result)) {
$result = 'English';
}
return $result;
}
/**
* Simplifies a string according to indexing rules.
*/
function tsearch_simplify($text) {
// Decode entities to UTF-8
$text = decode_entities($text);
// Call an external processor for word handling.
tsearch_invoke_preprocess($text);
// To improve searching for numerical data such as dates, IP addresses
// or version numbers, we consider a group of numerical characters
// separated only by punctuation characters to be one piece.
// This also means that searching for e.g. '20/03/1984' also returns
// results with '20-03-1984' in them.
// Readable regexp: ([number]+)[punctuation]+(?=[number])
$text = preg_replace('/(['. PREG_CLASS_NUMBERS .']+)['. PREG_CLASS_PUNCTUATION .']+(?=['. PREG_CLASS_NUMBERS .'])/u', '\1', $text);
// Remove all the HTML, since PostgreSQL doesn't do that for us.
$text = strip_tags($text);
// The dot, underscore and dash are simply removed. This allows meaningful
// search behavior with acronyms and URLs.
$text = preg_replace('/[._-]+/', '', $text);
// With the exception of the rules above, we consider all punctuation,
// marks, spacers, etc, to be a word boundary.
$text = preg_replace('/['. PREG_CLASS_SEARCH_EXCLUDE .']+/u', ' ', $text);
return $text;
}
/**
* Invokes hook_search_preprocess() in modules.
*/
function tsearch_invoke_preprocess(&$text) {
foreach (module_implements('search_preprocess') as $module) {
$text = module_invoke($module, 'search_preprocess', $text);
}
}
/**
* Change a node's changed timestamp to 'now' to force reindexing.
*
* @param $nid
* The nid of the node that needs reindexing.
*/
function tsearch_touch_node($nid) {
db_query("UPDATE {tsearch_node} SET updated = 0 WHERE nid = %d", $nid);
}
/**
* Implementation of hook_comment().
*/
function tsearch_comment($a1, $op) {
switch ($op) {
// Reindex the node when comments are added or changed
case 'insert':
case 'update':
case 'delete':
case 'publish':
case 'unpublish':
tsearch_touch_node(is_array($a1) ? $a1['nid'] : $a1->nid);
break;
}
}
/**
* Helper function for grabbing search keys.
*/
function tsearch_get_keys() {
static $return;
if (!isset($return)) {
// Extract keys as remainder of path
// Note: support old GET format of searches for existing links.
$path = explode('/', $_GET['q'], 3);
$keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys'];
$return = count($path) == 3 ? $path[2] : $keys;
}
return $return;
}
/**
* @defgroup search Search interface
* @{
* The Drupal search interface manages a global search mechanism.
*
* Modules may plug into this system to provide searches of different types of
* data. Most of the system is handled by search.module, so this must be enabled
* for all of the search features to work.
*
* There are three ways to interact with the search system:
* - Specifically for searching nodes, you can implement nodeapi('update index')
* and nodeapi('search result'). However, note that the search system already
* indexes all visible output of a node, i.e. everything displayed normally
* by hook_view() and hook_nodeapi('view'). This is usually sufficient.
* You should only use this mechanism if you want additional, non-visible data
* to be indexed.
* - Implement hook_search(). This will create a search tab for your module on
* the /search page with a simple keyword search form. You may optionally
* implement hook_search_item() to customize the display of your results.
* - Implement hook_update_index(). This allows your module to use Drupal's
* HTML indexing mechanism for searching full text efficiently.
*
* If your module needs to provide a more complicated search form, then you need
* to implement it yourself without hook_search(). In that case, you should
* define it as a local task (tab) under the /search page (e.g. /search/mymodule)
* so that users can easily find it.
*/
/**
* Render a search form.
*
* @param $action
* Form action. Defaults to "search".
* @param $keys
* The search string entered by the user, containing keywords for the search.
* @param $type
* The type of search to render the node for. Must be the name of module
* which implements hook_search(). Defaults to 'node'.
* @param $prompt
* A piece of text to put before the form (e.g. "Enter your keywords")
* @return
* An HTML string containing the search form.
*/
function tsearch_form(&$form_state, $action = '', $keys = '', $type = NULL, $prompt = NULL) {
// Add CSS
drupal_add_css(drupal_get_path('module', 'tsearch') .'/tsearch.css', 'module', 'all', FALSE);
if (!$action) {
$action = url('tsearch/'. $type);
}
if (is_null($prompt)) {
$prompt = t('Enter your keywords');
}
$form = array(
'#action' => $action,
'#attributes' => array('class' => 'tsearch-form'),
);
$form['module'] = array('#type' => 'value', '#value' => $type);
$form['basic'] = array('#type' => 'item', '#title' => $prompt);
$form['basic']['inline'] = array('#prefix' => '<div class="container-inline">', '#suffix' => '</div>');
$form['basic']['inline']['keys'] = array(
'#type' => 'textfield',
'#title' => '',
'#default_value' => $keys,
'#size' => $prompt ? 40 : 20,
'#maxlength' => 255,
);
// processed_keys is used to coordinate keyword passing between other forms
// that hook into the basic search form.
$form['basic']['inline']['processed_keys'] = array('#type' => 'value', '#value' => array());
$form['basic']['inline']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
return $form;
}
/**
* Form builder; Output a search form for the search block and the theme's search box.
*
* @ingroup forms
* @see tsearch_box_form_submit()
* @see theme_search_box_form()
*/
function tsearch_box(&$form_state, $form_id) {
$form[$form_id] = array(
'#title' => t('Search this site'),
'#type' => 'textfield',
'#size' => 15,
'#default_value' => '',
'#attributes' => array('title' => t('Enter the terms you wish to search for.')),
);
$form['submit'] = array('#type' => 'submit', '#value' => t('Search'));
$form['#submit'][] = 'tsearch_box_form_submit';
$form['#validate'][] = 'tsearch_box_form_validate';
return $form;
}
/**
* Process a block search form submission.
*/
function tsearch_box_form_submit($form, &$form_state) {
$form_id = $form['form_id']['#value'];
$form_state['redirect'] = 'tsearch/node/'. trim($form_state['values'][$form_id]);
}
/**
* Process variables for search-theme-form.tpl.php.
*
* The $variables array contains the following arguments:
* - $form
*
* @see search-theme-form.tpl.php
*/
function template_preprocess_tsearch_theme_form(&$variables) {
$variables['search'] = array();
$hidden = array();
// Provide variables named after form keys so themers can print each element independently.
foreach (element_children($variables['form']) as $key) {
$type = $variables['form'][$key]['#type'];
if ($type == 'hidden' || $type == 'token') {
$hidden[] = drupal_render($variables['form'][$key]);
}
else {
$variables['search'][$key] = drupal_render($variables['form'][$key]);
}
}
// Hidden form elements have no value to themers. No need for separation.
$variables['search']['hidden'] = implode($hidden);
// Collect all form elements to make it easier to print the whole form.
$variables['search_form'] = implode($variables['search']);
}
/**
* Process variables for search-block-form.tpl.php.
*
* The $variables array contains the following arguments:
* - $form
*
* @see search-block-form.tpl.php
*/
function template_preprocess_tsearch_block_form(&$variables) {
$variables['search'] = array();
$hidden = array();
// Provide variables named after form keys so themers can print each element independently.
foreach (element_children($variables['form']) as $key) {
$type = $variables['form'][$key]['#type'];
if ($type == 'hidden' || $type == 'token') {
$hidden[] = drupal_render($variables['form'][$key]);
}
else {
$variables['search'][$key] = drupal_render($variables['form'][$key]);
}
}
// Hidden form elements have no value to themers. No need for separation.
$variables['search']['hidden'] = implode($hidden);
// Collect all form elements to make it easier to print the whole form.
$variables['search_form'] = implode($variables['search']);
}
/**
* Perform a standard search on the given keys, and return the formatted results.
*/
function tsearch_data($keys = NULL, $type = 'node') {
$count_query = <<<SQL
SELECT COUNT(t.nid)
FROM tsearch_node AS t, plainto_tsquery('%s') query
WHERE query @@ node_tsvector
SQL;
$query = <<<SQL
SELECT t.nid, ts_rank_cd(node_tsvector, query, 16) AS rank
FROM tsearch_node AS t, plainto_tsquery('%s') query
WHERE query @@ node_tsvector
ORDER BY rank DESC
SQL;
$results = array();
$query = pager_query($query, 10, 0, $count_query, $keys);
while ($row = db_fetch_array($query)) {
$results[] = _tsearch_render_node_result($row['nid'], $row['rank']);
}
// Get a highlighted snippet of the search results
$results = _tsearch_get_headlines($results, $keys);
return $results;
}
/**
* @} End of "defgroup search".
*/
function tsearch_forms() {
$forms['tsearch_theme_form']= array(
'callback' => 'tsearch_box',
'callback arguments' => array('tsearch_theme_form'),
);
$forms['tsearch_block_form']= array(
'callback' => 'tsearch_box',
'callback arguments' => array('tsearch_block_form'),
);
return $forms;
}
/**
* Render the matched nodes with their data
*
* @param integer $nid
* The node ID of the matched node.
* @param float $rank
* The search rank/score
* @return array
* Structured result data array.
*/
function _tsearch_render_node_result($nid, $rank) {
// Build the node body.
$node = node_load($nid);
$node->build_mode = NODE_BUILD_SEARCH_RESULT;
$node = node_build_content($node, FALSE, FALSE);
$node->body = drupal_render($node->content);
// Fetch comments for snippet.
$node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
// Fetch terms for snippet.
$node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
$extra = node_invoke_nodeapi($node, 'search result');
$data = array(
'link' => url('node/'. $nid, array('absolute' => TRUE)),
'type' => check_plain(node_get_types('name', $node)),
'title' => $node->title,
'user' => theme('username', $node),
'date' => $node->changed,
'node' => $node,
'extra' => $extra,
'score' => $rank,
'snippet' => ''// search_excerpt($keys, $node->body),
);
return $data;
}
/**
* Get a highlighted snippet of the search result
*
* Uses PostgreSQLs ts_headline function to generate the snippet
*
* @param array $results
* The search results we're working on.
* @param string $keys
* The current search keys
*/
function _tsearch_get_headlines($results, $keys) {
$headline_query = <<<SQL
SELECT ts_headline('%s', '%s', plainto_tsquery('%s'),
'StartSel = <strong>, StopSel = </strong>, MaxWords=100'
);
SQL;
foreach ($results as $key => $data) {
$lan = drupal_strtolower(tsearch_node_language_name($node));
$results[$key]['snippet'] = db_result(db_query($headline_query, $lan,
$data['node']->body, $keys));
}
return $results;
}