views_fastsearch.module

Tracking 5.x-2.x branch
  1. drupal
    1. 5 contributions/views_fastsearch/views_fastsearch.module

Add a views filter that allows nodes to be filtered quickly using part of the search module indexes.

This module was originally based on http://drupal.org/node/70884, however, it extends this with support for AND/OR, exception terms, quoted terms, and sorting by score.

However, that method was found to be flawed. It uses multiple joins to find matching AND terms. It works reasonably well, but is prone to slowness on large sites, especially when 5 or more terms were used. It is also flawed because MySQL has a 21 term join limit, and thus this wouldn't work at all with that many terms.

A newer faster method was discovered (credit to Moshe Weitzman) that uses GROUP BY and HAVING to find matching terms. The query relies on unique values in the search_index table, and since this is not always the case, use of this method requires a unique index be created. I hope that this becomes the Drupal default soon, however, until such time, you must first created this index yourself:

ALTER IGNORE TABLE search_index ADD UNIQUE INDEX (sid, word, type, fromsid);

See also http://drupal.org/node/143160.

Functions & methods

NameDescription
theme_views_fastsearch_display
theme_views_fastsearch_item
views_fastsearch_form_alterImplement hook_form_alter.
views_fastsearch_search_rankingImplement the search_ranking callback
views_fastsearch_validate
views_fastsearch_views_default_viewsImplementation of hook_views_default_views()
views_fastsearch_views_handler_search_indexCustom filter for SEARCH operations
views_fastsearch_views_handler_sort_scoreCustom sort for SEARCH operations
views_fastsearch_views_query_handler_field_scoreCustom field query handler to generate the score field
views_fastsearch_views_style_pluginsImplementation of hook_views_style_plugins()
views_fastsearch_views_tablesImplementation of views hook_views_tables()
_views_fastsearch_get_rankings
_views_fastsearch_queryThis is the original views_fastsearch that used multiple joins to find matching AND terms. It worked reasonably well, but is prone to slowness on large sites, especially when 5 or more terms were used. It is also flawed because MySQL has a 21 term…
_views_fastsearch_query_unique
_views_fastsearch_score

File

View source
  1. <?php
  2. /** @file
  3. * Add a views filter that allows nodes to be filtered quickly
  4. * using part of the search module indexes.
  5. *
  6. * This module was originally based on http://drupal.org/node/70884,
  7. * however, it extends this with support for AND/OR, exception terms,
  8. * quoted terms, and sorting by score.
  9. *
  10. * However, that method was found to be flawed. It uses multiple joins to find
  11. * matching AND terms. It works reasonably well, but is prone to slowness on
  12. * large sites, especially when 5 or more terms were used. It is also flawed
  13. * because MySQL has a 21 term join limit, and thus this wouldn't work at all
  14. * with that many terms.
  15. *
  16. * A newer faster method was discovered (credit to Moshe Weitzman) that uses
  17. * GROUP BY and HAVING to find matching terms. The query relies on unique
  18. * values in the search_index table, and since this is not always the case,
  19. * use of this method requires a unique index be created. I hope that this
  20. * becomes the Drupal default soon, however, until such time, you must first
  21. * created this index yourself:
  22. *
  23. * ALTER IGNORE TABLE search_index ADD UNIQUE INDEX (sid, word, type, fromsid);
  24. *
  25. * See also http://drupal.org/node/143160.
  26. */
  27. /**
  28. * Implementation of views hook_views_tables()
  29. */
  30. function views_fastsearch_views_tables() {
  31. $tables['search_index'] = array(
  32. 'name' => 'search_index',
  33. 'provider' => 'internal',
  34. 'fields' => array(
  35. 'score' => array(
  36. 'name' => t('Search: Score'),
  37. 'sortable' => FALSE,
  38. 'query_handler' => 'views_fastsearch_views_query_handler_field_score',
  39. 'help' => t('Return the search score relevance value.'),
  40. 'option' => 'string',
  41. 'notafield' => TRUE,
  42. ),
  43. ),
  44. 'join' => array(
  45. 'left' => array(
  46. 'table' => 'node',
  47. 'field' => 'nid'
  48. ),
  49. 'right' => array(
  50. 'field' => 'sid'
  51. ),
  52. ),
  53. 'filters' => array(
  54. 'word' => array(
  55. 'name' => t('Search: Fast Index'),
  56. 'operator' => array('=' => 'AND', 'AND+' => 'AND (empty all)', 'OR' => 'OR'),
  57. 'handler' => 'views_fastsearch_views_handler_search_index',
  58. 'option' => 'string',
  59. 'help' => t('replacement search filter that is faster than the default search.'),
  60. ),
  61. ),
  62. 'sorts' => array(
  63. 'score' => array(
  64. 'name' => t('Search: Score'),
  65. 'handler' => 'views_fastsearch_views_handler_sort_score',
  66. 'help' => t('Sort by the search word\'s score relevance.'),
  67. ),
  68. ),
  69. );
  70. $tables['search_dataset'] = array(
  71. 'name' => 'search_dataset',
  72. 'provider' => 'internal',
  73. 'join' => array(
  74. 'left' => array(
  75. 'table' => 'node',
  76. 'field' => 'nid'
  77. ),
  78. 'right' => array(
  79. 'field' => 'sid'
  80. ),
  81. ),
  82. );
  83. return $tables;
  84. }
  85. /**
  86. * Custom filter for SEARCH operations
  87. */
  88. function views_fastsearch_views_handler_search_index($op, $filter, $filterinfo, &$query) {
  89. switch ($op) {
  90. case 'handler':
  91. $word_count = 0;
  92. if (!empty($filter['value'])) {
  93. // use the newer fast search if the Dup indexes have been repaired
  94. $search_index_unique = variable_get('search_index_unique', 0);
  95. // walk through each of the values
  96. // building the AND, OR, and exclusion terms
  97. foreach (explode(' ', $filter['value']) as $value) {
  98. // OR applies to the next value only
  99. $upper_value = strtoupper($value);
  100. if ($upper_value == 'OR') {
  101. // the initial AND assumption was wrong,
  102. // convert the leading term to an OR term
  103. if (isset($values['AND']) && count($values['AND']) == 1) {
  104. $values['OR'] = $values['AND'];
  105. unset($values['AND']);
  106. }
  107. $sqlop = ' OR ';
  108. continue;
  109. }
  110. // throw out too small words
  111. if (drupal_strlen($value) < variable_get('minimum_word_size', 3)) {
  112. continue;
  113. }
  114. // we've got something to search on!
  115. $word_count ++;
  116. if ($sqlop == ' OR ' || ($filter['operator'] == 'OR' && $word_count > 1)) {
  117. $values['OR'][] = $value;
  118. }
  119. else { // implied AND
  120. // look for an end to a quoted phrase
  121. if (isset($in_quote)) {
  122. if (substr($value, -1) != $in_quote) {
  123. $quote_value .= ' '. $value;
  124. }
  125. else {
  126. if (!$search_index_unique) {
  127. $tnc = $query->add_table('search_dataset', TRUE);
  128. $tablename = $query->get_table_name('search_dataset', $tnc);
  129. $and_clause[] = "$tablename.data like '%%%s%%'";
  130. }
  131. $values['AND'][] = $quote_value .' '. drupal_substr($value, 0, -1);
  132. unset($in_quote);
  133. }
  134. continue;
  135. }
  136. $first_char = drupal_substr($value, 0, 1);
  137. switch ($first_char) {
  138. case '-': // values starting with a - are exlusion terms
  139. $values['EXCLUDE'][] = drupal_substr($value, 1);
  140. break;
  141. case '"': // begin quoted phrase
  142. case '\'':
  143. if (substr($value, -1) == $first_char) {
  144. $value = drupal_substr($value, 1, -2);
  145. }
  146. else {
  147. $in_quote = $first_char;
  148. $quote_value = drupal_substr($value, 1);
  149. break;
  150. }
  151. // FALLTHROUGH
  152. default:
  153. if (!$search_index_unique) {
  154. $tnc = $query->add_table('search_index', TRUE);
  155. $tablename = $query->get_table_name('search_index', $tnc);
  156. $extra['AND'][] = "$tablename.word='%s'";
  157. if ($filter['options']) {
  158. $tablename = $query->get_table_name('search_index', $tnc);
  159. $query->add_where("$tablename.type='%s'", $filter['options']);
  160. }
  161. }
  162. $values['AND'][] = $value;
  163. break;
  164. }
  165. }
  166. $sqlop = ' AND ';
  167. }
  168. }
  169. // if there aren't any words
  170. if ($word_count == 0) {
  171. $query->add_where($filter['operator'] == 'AND+' ? '1' : '0');
  172. return;
  173. }
  174. // 'simplify' the search terms, which calls the preprocess
  175. //
  176. // also, the Drupal search engine doesn't index quote characters,
  177. // so remove them
  178. foreach ($values as $op => $value) {
  179. foreach ($values[$op] as $index => $word) {
  180. $values[$op][$index] = str_replace('"', '', search_simplify($word));
  181. }
  182. }
  183. // NOTE: using global to pass values to theme_views_fastsearch_display
  184. global $search_keys;
  185. $search_keys = $values;
  186. // modify the query
  187. if ($search_index_unique) {
  188. _views_fastsearch_query_unique($query, $values, $extra);
  189. }
  190. else {
  191. _views_fastsearch_query($query, $values, $extra);
  192. }
  193. // Log the search keys:
  194. $type = 'views_fastsearch';
  195. if ($filter['operator'] != '=') {
  196. $type .= ' '. $filter['operator'];
  197. }
  198. watchdog('search', t('%keys (%type).', array('%keys' => $filter['value'], '%type' => $type)), WATCHDOG_NOTICE);
  199. }
  200. }
  201. function _views_fastsearch_query_unique(&$query, $values, $extra) {
  202. /**
  203. * merge all of the terms into a single array
  204. *
  205. * NOTE: our syntax isn't really good enough to support AND and OR terms.
  206. * We really should have just one or the other.
  207. *
  208. * But in order to support the older query method, the parsing has not been
  209. * cleaned up yet, and we have to merge the two terms here
  210. */
  211. $and = isset($values['AND']) ? $values['AND'] : array();
  212. $or = isset($values['OR']) ? $values['OR'] : array();
  213. $terms = array_merge($and, $or);
  214. // get the node rankings
  215. $rankings = _views_fastsearch_get_rankings();
  216. // create the SQL that joins everything above and queries for the terms
  217. $score = isset($rankings['score']) ? 'SUM('. implode(' + ', $rankings['score']) .')' : '1';
  218. $sql = 'SELECT n.nid, '. $score .' AS score FROM {node} n LEFT JOIN {search_index} i ON n.nid=i.sid';
  219. if (isset($rankings['join'])) {
  220. $sql .= ' '. implode(' ', $rankings['join']);
  221. }
  222. $sql .=
  223. ' WHERE i.fromsid=0 AND i.word IN ('.
  224. implode(',', array_fill(0, count($terms), "'%s'")) .')'.
  225. ' GROUP BY n.nid';
  226. // NOTE: no HAVING clause is needed for OR's and AND's with 1 term
  227. // because we using an INNER JOIN above - HAVING COUNT(*) >= 1 is implied
  228. if (!isset($values['OR']) || count($values['AND']) > 1) {
  229. $sql .= ' HAVING COUNT(*)='. count($terms);
  230. }
  231. if (isset($rankings['terms'])) {
  232. $terms = array_merge($rankings['terms'], $terms);
  233. }
  234. $join = array(
  235. 'type' => 'inner',
  236. 'left' => array('table' => 'node', 'field' => 'nid'),
  237. 'right' => array('field' => 'nid'),
  238. );
  239. if (method_exists($query, 'add_subquery')) {
  240. $query->add_subquery($sql, $terms, $join, 'temp_vfs');
  241. $query->add_field('score', 'temp_vfs');
  242. }
  243. elseif (db_query_temporary($sql, $terms, 'temp_vfs')) {
  244. $query->add_table('temp_vfs', FALSE, 1, $join);
  245. $query->add_field('score', 'temp_vfs');
  246. }
  247. // add the exclusion clause
  248. if (isset($values['EXCLUDE'])) {
  249. // use a JOIN instead of the sub-SELECT, when possible
  250. $extra['EXCLUDE'] = array_fill(0, count($values['EXCLUDE']), "vfsxe.word = '%s'");
  251. $sql = "(SELECT vfsne.nid FROM node vfsne LEFT join search_index vfsxe on vfsne.nid=vfsxe.sid WHERE vfsxe.fromsid = 0 AND (". implode(' OR ', $extra['EXCLUDE']) .") GROUP BY vfsne.nid HAVING COUNT(*) = ". count($exclude_clause) .")";
  252. $join = array(
  253. 'left' => array('table' => 'node', 'field' => 'nid'),
  254. 'right' => array('field' => 'nid'),
  255. );
  256. if (method_exists($query, 'add_subquery')) {
  257. $query->add_subquery($sql, array(), $join, 'temp_vfs_exclude');
  258. }
  259. elseif (db_query_temporary($sql, $values['EXCLUDE'], 'temp_vfs_exclude')) {
  260. $query->add_table('temp_vfs_exclude', FALSE, 1, $join);
  261. }
  262. $query->add_where('temp_vfs_exclude.nid IS NULL');
  263. }
  264. }
  265. function _views_fastsearch_get_rankings() {
  266. $rankings = array();
  267. if ($ranking = module_invoke_all('search_ranking')) {
  268. $used_ranking = array();
  269. foreach ($ranking as $rank => $values) {
  270. if ($weight = variable_get($rank, 5)) {
  271. $used_ranking["$rank|$weight"] = $values;
  272. }
  273. }
  274. foreach ($used_ranking as $index => $values) {
  275. list(, $weight) = explode('|', $index);
  276. // if the join doesn't already exist, add it
  277. if (!isset($rankings['join'][$values['join']])) {
  278. $rankings['join'][$values['join']] = $values['join'];
  279. }
  280. // add the weighted score multiplier value, handle NULL gracefully
  281. $rankings['score'][] = '%f * COALESCE(('. $values['score'] .'), 0)';
  282. // add the the weighted score multiplier value
  283. $rankings['terms'][] = $weight / count($used_ranking);
  284. // add the other terms
  285. if (isset($values['terms'])) {
  286. $rankings['terms'] = array_merge($rankings['terms'], $values['terms']);
  287. }
  288. }
  289. }
  290. return $rankings;
  291. }
  292. /**
  293. * Implement the search_ranking callback
  294. *
  295. * NOTE: this is a first draft
  296. * - these might be better in include files, one per module
  297. * - the array definition could be cleaned up
  298. */
  299. function views_fastsearch_search_ranking() {
  300. $ranking = array();
  301. // get the word relevance weight
  302. $ranking['node_rank_relevance'] = array(
  303. 'join' => 'LEFT JOIN {search_total} t ON i.word=t.word',
  304. 'score' => 'i.score * t.count',
  305. );
  306. // get the recent weight
  307. if ($node_cron_last = variable_get('node_cron_last', 0)) {
  308. if (module_exists('statistics')) {
  309. $ranking['node_rank_recent'] = array(
  310. 'join' => 'LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid',
  311. 'score' => 'POW(2, GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0) - %d) * 6.43e-8)',
  312. 'terms' => array($node_cron_last),
  313. );
  314. }
  315. else {
  316. $ranking['node_rank_recent'] = array(
  317. 'score' => 'POW(2, GREATEST(n.created, n.changed) - %d) * 6.43e-8',
  318. 'terms' => array($node_cron_last),
  319. );
  320. }
  321. }
  322. // get the comment weight
  323. if (module_exists('comment')) {
  324. $ranking['node_rank_comments'] = array(
  325. 'join' => 'LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid',
  326. 'score' => '2.0 - 2.0 / (1.0 + c.comment_count * %f)',
  327. 'terms' => array(variable_get('node_cron_comments_scale', 0)),
  328. );
  329. }
  330. // get the statistics weight
  331. if (module_exists('statistics') && variable_get('statistics_count_content_views', 0)) {
  332. $ranking['node_rank_views'] = array(
  333. 'join' => 'LEFT JOIN {node_counter} nc ON nc.nid = i.sid',
  334. 'score' => '2.0 - 2.0 / (1.0 + nc.totalcount * %f)',
  335. 'terms' => array(variable_get('node_cron_views_scale', 0)),
  336. );
  337. }
  338. return $ranking;
  339. }
  340. /**
  341. * This is the original views_fastsearch that used multiple joins to find
  342. * matching AND terms. It worked reasonably well, but is prone to slowness on
  343. * large sites, especially when 5 or more terms were used. It is also flawed
  344. * because MySQL has a 21 term join limit, and thus this wouldn't work at all
  345. * with that many terms.
  346. *
  347. * This option will be removed as soon as Drupal adopts the unique search_index
  348. * - see http://drupal.org/node/143160
  349. */
  350. function _views_fastsearch_query(&$query, $values, $extra) {
  351. // add the AND terms
  352. if (isset($extra['AND'])) {
  353. $conditions[] = '('. implode(' AND ', $extra['AND']) .')';
  354. }
  355. $arguments = $values['AND'];
  356. // add the OR terms
  357. if (isset($values['OR'])) {
  358. foreach ($values['OR'] as $value) {
  359. $conditions[] = '('. implode(' AND ', $extra['AND']) .')';
  360. $arguments = array_merge($arguments, array_fill(0, count($values['AND']), $value));
  361. }
  362. }
  363. if (isset($conditions)) {
  364. $query->add_where(implode(' OR ', $conditions), $arguments);
  365. }
  366. // add the exclusion clause
  367. if (isset($values['EXCLUDE'])) {
  368. /**
  369. * NOTE: that this should be done with a join instead of a subselect,
  370. * but given that this code is being phased out in favor of the unique
  371. * index, this is what we're left with.
  372. */
  373. $exclude_clause = implode(', ', array_fill(0, count($values['EXCLUDE']), "'%s'"));
  374. $query->add_where("node.nid NOT IN (SELECT sid FROM search_index WHERE word in (". $exclude_clause ."))", $values['EXCLUDE']);
  375. }
  376. // there are dups in the index so we need distinct results
  377. $query->set_distinct();
  378. }
  379. /**
  380. * Custom field query handler to generate the score field
  381. */
  382. function views_fastsearch_views_query_handler_field_score($field, $fieldinfo, &$query) {
  383. if (variable_get('search_index_unique', 0)) {
  384. if ($score_field = _views_fastsearch_score($query)) {
  385. $query->add_field($score_field, NULL, 'score');
  386. }
  387. }
  388. }
  389. function _views_fastsearch_score($query) {
  390. if (isset($query->subquery['temp_vfs'])) {
  391. $orderby[] = 'temp_vfs.score';
  392. }
  393. foreach ($query->joins as $table => $tinfo) {
  394. if ($table == 'search_index') {
  395. $orderby[] = 'search_index.score';
  396. }
  397. }
  398. if (isset($orderby)) {
  399. if (count($orderby) > 1) {
  400. $orderby_clause = 'COALESCE('. implode(',', $orderby) .')';
  401. }
  402. else {
  403. $orderby_clause = $orderby[0];
  404. }
  405. return $orderby_clause;
  406. }
  407. }
  408. /**
  409. * Custom sort for SEARCH operations
  410. */
  411. function views_fastsearch_views_handler_sort_score($op, &$query, $sortinfo, $sort) {
  412. if (variable_get('search_index_unique', 0)) {
  413. if ($score_field = _views_fastsearch_score($query)) {
  414. $query->add_field($score_field, NULL, 'score');
  415. $query->orderby[] = 'score '. $sort['sortorder'];
  416. }
  417. }
  418. else {
  419. if (isset($query->tables['search_index'])) {
  420. for ($i = 1; $i <= $query->tables['search_index']; $i ++) {
  421. $tnc = $i;
  422. $tnc = intval($tnc) > 1 ? $tnc: "";
  423. $scores[] = "search_index$tnc.score";
  424. }
  425. $score = implode('+', $scores);
  426. $query->orderby[] = "$score $sort[sortorder]";
  427. }
  428. }
  429. }
  430. /**
  431. * Implementation of hook_views_default_views()
  432. */
  433. function views_fastsearch_views_default_views() {
  434. $view = new stdClass();
  435. $view->name = 'views_fastsearch';
  436. $view->description = 'Search';
  437. $view->access = array (
  438. );
  439. $view->view_args_php = '';
  440. $view->page = TRUE;
  441. $view->page_title = 'Search';
  442. $view->page_header = '';
  443. $view->page_header_format = '1';
  444. $view->page_footer = '';
  445. $view->page_footer_format = '1';
  446. $view->page_empty = t('Your search yielded no results.');
  447. $view->page_empty = '';
  448. $view->page_empty_format = '1';
  449. $view->page_type = 'search';
  450. $view->url = 'search/fast';
  451. $view->use_pager = TRUE;
  452. $view->nodes_per_page = '10';
  453. $view->sort = array (
  454. array (
  455. 'tablename' => 'search_index',
  456. 'field' => 'score',
  457. 'sortorder' => 'DESC',
  458. 'options' => '',
  459. ),
  460. );
  461. $view->argument = array (
  462. );
  463. $view->field = array (
  464. array (
  465. 'tablename' => 'search_index',
  466. 'field' => 'score',
  467. 'label' => '',
  468. ),
  469. );
  470. $view->filter = array (
  471. array (
  472. 'tablename' => 'search_index',
  473. 'field' => 'word',
  474. 'operator' => '=',
  475. 'options' => '',
  476. 'value' => '',
  477. ),
  478. );
  479. $view->exposed_filter = array (
  480. array (
  481. 'tablename' => 'search_index',
  482. 'field' => 'word',
  483. 'label' => '',
  484. 'optional' => '0',
  485. 'is_default' => '0',
  486. 'operator' => '1',
  487. 'single' => '0',
  488. ),
  489. );
  490. $view->requires = array(search_index);
  491. $views[$view->name] = $view;
  492. return $views;
  493. }
  494. /**
  495. * Implement hook_form_alter.
  496. */
  497. function views_fastsearch_form_alter($form_id, &$form) {
  498. if ($form_id == 'search_theme_form') {
  499. if ($view = views_get_view('views_fastsearch')) {
  500. $views_status = variable_get('views_defaults', array());
  501. if ($views_status[$view->name] == 'enabled' || (!$view->disabled && $views_status[$view->name] != 'disabled')) {
  502. /* @TODO: this isn't quite working yet
  503. $form['filter0'] = $form[$form_id .'_keys'];
  504. $form['filter0']['#weight'] = -1;
  505. unset($form[$form_id .'_keys']);
  506. unset($form['#base']);
  507. unset($form['#submit']['search_box_form_submit']);
  508. $form['#action'] = url($view->url);
  509. */
  510. }
  511. }
  512. }
  513. elseif ($form_id == 'search_admin_settings') {
  514. global $db_type;
  515. if ($db_type == 'mysql') {
  516. $result = db_fetch_array(db_query("SHOW TABLE STATUS LIKE 'search_index'"));
  517. if ($result['Engine'] == 'InnoDB') {
  518. $table = db_prefix_tables("{search_index}");
  519. drupal_set_message('The '. $table .' table should not use innodb for performance reasons -- execute "ALTER TABLE '. $table .' ENGINE=MyISAM". On production sites, the search.module should be disabled while the table is being altered and re-enabled when the alter is complete.', 'error');
  520. }
  521. }
  522. // add any additional node ranking
  523. if ($ranking = module_invoke_all('search_ranking')) {
  524. // ignore the standard node_rankings
  525. foreach (views_fastsearch_search_ranking() as $rank => $values) {
  526. unset($ranking[$rank]);
  527. }
  528. foreach ($ranking as $rank => $values) {
  529. $form['content_ranking']['factors'][$rank] = array(
  530. '#type' => 'select',
  531. '#title' => $values['description'],
  532. '#options' => range(0, 10),
  533. '#default_value' => variable_get($rank, 5),
  534. );
  535. }
  536. }
  537. $newform = array(
  538. 'views_fastsearch' => array(
  539. '#type' => 'fieldset',
  540. '#title' => t('views_fastsearch'),
  541. '#collapsible' => TRUE,
  542. '#collapsed' => FALSE,
  543. 'search_index_unique' => array(
  544. '#type' => 'select',
  545. '#title' => t('search_index'),
  546. '#options' => array(0 => t('Default Drupal Installation'), 1 => t('No duplicates (Unique Index Exists)')),
  547. '#default_value' => variable_get('search_index_unique', 0),
  548. '#description' => t('The {search_index} may have duplicate entries caused by overlapping cron runs. This happens less frequently since Drupal 5.x, but still can occur. The best solution is to create a UNIQUE index using "ALTER IGNORE TABLE search_index ADD UNIQUE INDEX (sid, word, type, fromsid)". If you have created this index or if you know your site does not have duplicates, select UNIQUE so that the faster views_fastsearch algorithm may be used. See <a href="@url">%nid</a>', array('@url' => 'http://drupal.org/node/143160', '%nid' => '#143160')),
  549. ),
  550. ),
  551. );
  552. // add the option before the buttons
  553. $pos = array_search('buttons', array_keys($form));
  554. $form = array_merge(array_slice($form, 0, $pos), $newform, array_slice($form, $pos));
  555. }
  556. }
  557. /**
  558. * Implementation of hook_views_style_plugins()
  559. */
  560. function views_fastsearch_views_style_plugins() {
  561. $plugins = array(
  562. 'search' => array(
  563. 'name' => t('Search Results'),
  564. 'theme' => 'views_fastsearch_display',
  565. 'summary_theme' => 'views_fastsearch_display',
  566. 'validate' => 'views_fastsearch_validate',
  567. 'needs_fields' => TRUE,
  568. ),
  569. );
  570. return $plugins;
  571. }
  572. function views_fastsearch_validate($type, $view, $form) {
  573. if (isset($view['field']['count'])) {
  574. for ($i = 0; $i < $view['field']['count']; $i ++) {
  575. if ($view['field'][$i]['id'] == 'search_index.score') {
  576. return;
  577. }
  578. }
  579. }
  580. form_error($form[$type .'-info'][$type .'_type'], t('Search Results requires the search score field.'));
  581. }
  582. function theme_views_fastsearch_display(&$view, &$items, $type) {
  583. drupal_add_css(drupal_get_path('module', 'search') .'/search.css', 'module', 'all', FALSE);
  584. if (isset($items) && is_array($items) && count($items)) {
  585. // NOTE: using global to pass values from
  586. // views_fastsearch_views_handler_search_index
  587. global $search_keys;
  588. if (isset($search_keys)) {
  589. $keys = array();
  590. foreach ($search_keys as $value) {
  591. $keys = array_merge($keys, $value);
  592. }
  593. $excerpt_keys = implode(' ', $keys);
  594. }
  595. $output = '<h2>'. t('Search Results') .'</h2>';
  596. $output .= '<dl class="search-results">';
  597. foreach ($items as $item) {
  598. // Build the node body.
  599. $node = node_load($item->nid);
  600. $node = node_build_content($node, FALSE, FALSE);
  601. $node->body = drupal_render($node->content);
  602. // Fetch comments for snippet
  603. $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
  604. // Fetch terms for snippet
  605. $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
  606. $extra = node_invoke_nodeapi($node, 'search result');
  607. $entry = array(
  608. 'link' => url('node/'. $item->nid, NULL, NULL, TRUE),
  609. 'type' => node_get_types('name', $node),
  610. 'title' => $node->title,
  611. 'user' => theme('username', $node),
  612. 'date' => $node->changed,
  613. 'node' => $node,
  614. 'view' => $view,
  615. 'extra' => $extra,
  616. 'score' => $item->score,
  617. 'snippet' => search_excerpt($excerpt_keys, $node->body)
  618. );
  619. $output .= theme('views_fastsearch_item', $entry, $type);
  620. }
  621. $output .= '</dl>';
  622. return $output;
  623. }
  624. }
  625. function theme_views_fastsearch_item($entry, $type) {
  626. if (!empty($entry['score'])) {
  627. $entry['extra'][] = 'score: '. round($entry['score'], 4);
  628. }
  629. return theme('search_item', $entry, $type);
  630. }