/[drupal]/contributions/modules/apachesolr/apachesolr.module
ViewVC logotype

Contents of /contributions/modules/apachesolr/apachesolr.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.14 - (show annotations) (download) (as text)
Mon Jun 29 23:23:07 2009 UTC (4 months, 3 weeks ago) by pwolanin
Branch: MAIN
CVS Tags: HEAD
Changes since 1.13: +298 -96 lines
File MIME type: text/x-php
sync with DRUPAL-6--1
1 <?php
2 // $Id: apachesolr.module,v 1.1.2.12.2.148 2009/06/19 20:31:58 pwolanin Exp $
3
4 /**
5 * @file
6 * Integration with the Apache Solr search application.
7 */
8
9 /**
10 * Implementation of hook_menu().
11 */
12 function apachesolr_menu() {
13 $items = array();
14 $items['admin/settings/apachesolr'] = array(
15 'title' => 'Apache Solr',
16 'description' => 'Administer Apache Solr.',
17 'page callback' => 'drupal_get_form',
18 'page arguments' => array('apachesolr_settings'),
19 'access callback' => 'user_access',
20 'access arguments' => array('administer site configuration'),
21 'file' => 'apachesolr.admin.inc',
22 );
23 $items['admin/settings/apachesolr/settings'] = array(
24 'title' => 'Settings',
25 'weight' => -10,
26 'access arguments' => array('administer site configuration'),
27 'file' => 'apachesolr.admin.inc',
28 'type' => MENU_DEFAULT_LOCAL_TASK,
29 );
30 $items['admin/settings/apachesolr/enabled-filters'] = array(
31 'title' => 'Enabled filters',
32 'page callback' => 'drupal_get_form',
33 'page arguments' => array('apachesolr_enabled_facets_form'),
34 'weight' => -7,
35 'access arguments' => array('administer site configuration'),
36 'file' => 'apachesolr.admin.inc',
37 'type' => MENU_LOCAL_TASK,
38 );
39 $items['admin/settings/apachesolr/index'] = array(
40 'title' => 'Search index',
41 'page callback' => 'apachesolr_index_page',
42 'access arguments' => array('administer site configuration'),
43 'weight' => -8,
44 'file' => 'apachesolr.admin.inc',
45 'type' => MENU_LOCAL_TASK,
46 );
47 $items['admin/settings/apachesolr/index/delete/confirm'] = array(
48 'title' => 'Confirm index deletion',
49 'page callback' => 'drupal_get_form',
50 'page arguments' => array('apachesolr_delete_index_confirm'),
51 'access arguments' => array('administer site configuration'),
52 'file' => 'apachesolr.admin.inc',
53 'type' => MENU_CALLBACK,
54 );
55 $items['admin/reports/apachesolr'] = array(
56 'title' => 'Apache Solr Search index',
57 'page callback' => 'apachesolr_index_report',
58 'access arguments' => array('access site reports'),
59 'file' => 'apachesolr.admin.inc',
60 );
61 $items['admin/reports/apachesolr/index'] = array(
62 'title' => 'Search index',
63 'file' => 'apachesolr.admin.inc',
64 'type' => MENU_DEFAULT_LOCAL_TASK,
65 );
66 $items['admin/settings/apachesolr/mlt/add_block'] = array(
67 'page callback' => 'drupal_get_form',
68 'page arguments' => array('apachesolr_mlt_add_block_form'),
69 'access arguments' => array('administer search'),
70 'file' => 'apachesolr.admin.inc',
71 'type' => MENU_CALLBACK,
72 );
73 $items['admin/settings/apachesolr/mlt/delete_block/%'] = array(
74 'page callback' => 'drupal_get_form',
75 'page arguments' => array('apachesolr_mlt_delete_block_form', 5),
76 'access arguments' => array('administer search'),
77 'file' => 'apachesolr.admin.inc',
78 'type' => MENU_CALLBACK,
79 );
80 return $items;
81 }
82
83 /**
84 * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.)
85 * Depending on the admin settings, possibly redirect to Drupal's core search.
86 *
87 * @param $search_name
88 * The name of the search implementation.
89 *
90 * @param $querystring
91 * The search query that was issued at the time of failure.
92 */
93 function apachesolr_failure($search_name, $querystring) {
94 $fail_rule = variable_get('apachesolr_failure', 'show_error');
95
96 switch ($fail_rule) {
97 case 'show_error':
98 drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error');
99 break;
100 case 'show_drupal_results':
101 drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name)));
102 drupal_goto('search/node/' . drupal_urlencode($querystring));
103 break;
104 case 'show_no_results':
105 return;
106 }
107 }
108
109 /**
110 * Implementation of hook_requirements().
111 */
112 function apachesolr_requirements($phase) {
113 // Ensure translations don't break at install time
114 $t = get_t();
115 if ($phase == 'runtime') {
116 $host = variable_get('apachesolr_host', 'localhost');
117 $port = variable_get('apachesolr_port', 8983);
118 $path = variable_get('apachesolr_path', '/solr');
119 $ping = FALSE;
120 try {
121 $solr = apachesolr_get_solr();
122 $ping = @$solr->ping(variable_get('apachesolr_ping_timeout', 4));
123 // If there is no $solr object, there is no server available, so don't continue.
124 if (!$ping) {
125 throw new Exception(t('No Solr instance available when checking requirements.'));
126 }
127 }
128 catch (Exception $e) {
129 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
130 }
131 $value = $ping ? $t('Your site has contacted the Apache Solr server.') : $t('Your site was unable to contact the Apache Solr server.');
132 $severity = $ping ? 0: 2;
133 $description = theme('item_list', array($t('Host: %host', array('%host' => $host)),
134 $t('Port: %port', array('%port' => $port)),
135 $t('Path: %path', array('%path' => $path))));
136 $requirements['apachesolr'] = array(
137 'title' => $t('Apache Solr'),
138 'value' => $value,
139 'description' => $description,
140 'severity' => $severity,
141 );
142 return $requirements;
143 }
144 }
145
146 /**
147 * Like $site_key in _update_refresh() - returns a site-specific hash.
148 */
149 function apachesolr_site_hash() {
150 if (!($hash = variable_get('apachesolr_site_hash', FALSE))) {
151 global $base_url;
152 $hash = substr(md5(md5($base_url . drupal_get_private_key() . 'apachesolr')), 0, 12);
153 variable_set('apachesolr_site_hash', $hash);
154 }
155 return $hash;
156 }
157
158 function apachesolr_document_id($id, $type = 'node') {
159 return apachesolr_site_hash() . "/$type/" . $id;
160 }
161
162 /**
163 * Implementation of hook_user().
164 *
165 * Mark nodes as needing re-indexing if the author name changes.
166 */
167 function apachesolr_user($op, &$edit, &$account) {
168 switch ($op) {
169 case 'update':
170 if (isset($edit['name']) && $account->name != $edit['name']) {
171 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid);
172 }
173 break;
174 }
175 }
176
177 /**
178 * Implementation of hook_taxonomy().
179 *
180 * Mark nodes as needing re-indexing if a term name changes.
181 */
182 function apachesolr_taxonomy($op, $type, $edit) {
183 if ($type == 'term' && ($op == 'update')) {
184 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']);
185 }
186 // TODO: the rest, such as term deletion.
187 }
188
189 /**
190 * Implementation of hook_comment().
191 *
192 * Mark nodes as needing re-indexing if comments are added or changed.
193 * Like search_comment().
194 */
195 function apachesolr_comment($edit, $op) {
196 $edit = (array) $edit;
197 switch ($op) {
198 // Reindex the node when comments are added or changed
199 case 'insert':
200 case 'update':
201 case 'delete':
202 case 'publish':
203 case 'unpublish':
204 apachesolr_mark_node($edit['nid']);
205 break;
206 }
207 }
208
209 /**
210 * Mark one node as needing re-indexing.
211 */
212 function apachesolr_mark_node($nid) {
213 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid);
214 }
215
216 /**
217 * Implementation of hook_node_type().
218 *
219 * Mark nodes as needing re-indexing if a node type name changes.
220 */
221 function apachesolr_node_type($op, $info) {
222 if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) {
223 // We cannot be sure we are going before or after node module.
224 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s' OR type = '%s')", time(), $info->old_type, $info->type);
225 }
226 }
227
228 /**
229 * Helper function for modules implmenting hook_search's 'status' op.
230 */
231 function apachesolr_index_status($namespace) {
232 list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
233
234 $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE asn.status = 1 " . $exclude_sql, $excluded_types));
235 $remaining = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 " . $exclude_sql, $args));
236 return array('remaining' => $remaining, 'total' => $total);
237 }
238
239 /**
240 * Returns last changed and last nid for an indexing namespace.
241 */
242 function apachesolr_get_last_index($namespace) {
243 $stored = variable_get('apachesolr_index_last', array());
244 return isset($stored[$namespace]) ? $stored[$namespace] : array('last_change' => 0, 'last_nid' => 0);
245 }
246
247 /**
248 * Clear a specific namespace's last changed and nid, or clear all.
249 */
250 function apachesolr_clear_last_index($namespace = '') {
251 if ($namespace) {
252 $stored = variable_get('apachesolr_index_last', array());
253 unset($stored[$namespace]);
254 variable_set('apachesolr_index_last', $stored);
255 }
256 else {
257 variable_del('apachesolr_index_last');
258 }
259 }
260
261 function _apachesolr_exclude_types($namespace) {
262 extract(apachesolr_get_last_index($namespace));
263 $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace);
264 $args = array($last_change, $last_change, $last_nid);
265 $join_sql = '';
266 $exclude_sql = '';
267 if ($excluded_types) {
268 $excluded_types = array_unique($excluded_types);
269 $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid ";
270 $exclude_sql = "AND n.type NOT IN(". db_placeholders($excluded_types, 'varchar') .") ";
271 $args = array_merge($args, $excluded_types);
272 }
273 return array($excluded_types, $args, $join_sql, $exclude_sql);
274 }
275
276 /**
277 * Returns an array of rows from a query based on an indexing namespace.
278 */
279 function apachesolr_get_nodes_to_index($namespace, $limit) {
280 $rows = array();
281 if (variable_get('apachesolr_read_only', 0)) {
282 return $rows;
283 }
284 list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
285 $result = db_query_range("SELECT asn.nid, asn.changed FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 ". $exclude_sql ."ORDER BY asn.changed ASC, asn.nid ASC", $args, 0, $limit);
286 while($row = db_fetch_object($result)) {
287 $rows[] = $row;
288 }
289 return $rows;
290 }
291
292 /**
293 * Function to handle the indexing of nodes.
294 *
295 * The calling function must supply a name space or track/store
296 * the timestamp and nid returned.
297 * Returns FALSE if no nodes were indexed (none found or error).
298 */
299 function apachesolr_index_nodes($rows, $namespace = '', $callback = 'apachesolr_add_node_document') {
300 if (!$rows) {
301 // Nothing to do.
302 return FALSE;
303 }
304
305 try {
306 // Get the $solr object
307 $solr = apachesolr_get_solr();
308 // If there is no server available, don't continue.
309 if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) {
310 throw new Exception(t('No Solr instance available during indexing.'));
311 }
312 }
313 catch (Exception $e) {
314 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
315 return FALSE;
316 }
317 include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc') ;
318 $documents = array();
319 $old_position = apachesolr_get_last_index($namespace);
320 $position = $old_position;
321
322 foreach ($rows as $row) {
323 // Variables to track the last item changed.
324 $position['last_change'] = $row->changed;
325 $position['last_nid'] = $row->nid;
326 $callback($documents, $row->nid, $namespace);
327 }
328
329 if (count($documents)) {
330 try {
331 watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents)));
332 // Chunk the adds by 20s
333 $docs_chunk = array_chunk($documents, 20);
334 foreach ($docs_chunk as $docs) {
335 $solr->addDocuments($docs);
336 }
337 // Set the timestamp to indicate an index update.
338 apachesolr_index_updated(time());
339 }
340 catch (Exception $e) {
341 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
342 return FALSE;
343 }
344 }
345
346 // Save the new position in case it changed.
347 if ($namespace && $position != $old_position) {
348 $stored = variable_get('apachesolr_index_last', array());
349 $stored[$namespace] = $position;
350 variable_set('apachesolr_index_last', $stored);
351 }
352
353 return $position;
354 }
355
356 /**
357 * Convert date from timestamp into ISO 8601 format.
358 * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
359 */
360 function apachesolr_date_iso($date_timestamp) {
361 return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp);
362 }
363
364 function apachesolr_delete_node_from_index($node) {
365 try {
366 $solr = apachesolr_get_solr();
367 $solr->deleteById(apachesolr_document_id($node->nid));
368 apachesolr_index_updated(time());
369 return TRUE;
370 }
371 catch (Exception $e) {
372 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
373 return FALSE;
374 }
375 }
376
377 /**
378 * Helper function to keep track of when the index has been updated.
379 */
380 function apachesolr_index_updated($updated = NULL) {
381 if (isset($updated)) {
382 if ($updated) {
383 variable_set('apachesolr_index_updated', (int) $updated);
384 }
385 else {
386 variable_del('apachesolr_index_updated');
387 }
388 }
389 return variable_get('apachesolr_index_updated', 0);
390 }
391
392 /**
393 * Implementation of hook_cron().
394 */
395 function apachesolr_cron() {
396 try {
397 $solr = apachesolr_get_solr();
398
399 // Check for unpublished content that wasn't deleted from the index.
400 $result = db_query("SELECT n.nid, n.status FROM {apachesolr_search_node} asn INNER JOIN {node} n ON n.nid = asn.nid WHERE asn.status != n.status");
401 while ($node = db_fetch_object($result)) {
402 _apachesolr_nodeapi_update($node, FALSE);
403 }
404
405 // Check for deleted content that wasn't deleted from the index.
406 $result = db_query("SELECT asn.nid FROM {apachesolr_search_node} asn LEFT JOIN {node} n ON n.nid = asn.nid WHERE n.nid IS NULL");
407 while ($node = db_fetch_object($result)) {
408 _apachesolr_nodeapi_delete($node, FALSE);
409 }
410
411 // Optimize the index (by default once a day).
412 $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24);
413 $last = variable_get('apachesolr_last_optimize', 0);
414 $time = time();
415 if ($optimize_interval && ($time - $last > $optimize_interval)) {
416 $solr->optimize(FALSE, FALSE);
417 variable_set('apachesolr_last_optimize', $time);
418 apachesolr_index_updated($time);
419 }
420
421 // Only clear the cache if the index changed.
422 // TODO: clear on some schedule if running multi-site.
423 $updated = apachesolr_index_updated();
424 if ($updated) {
425 $solr->clearCache();
426 // Re-populate the luke cache.
427 $solr->getLuke();
428 // TODO: an admin interface for setting this. Assume for now 5 minutes.
429 if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) {
430 // Clear the updated flag.
431 apachesolr_index_updated(FALSE);
432 }
433 }
434 }
435 catch (Exception $e) {
436 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) . ' in apachesolr_cron', NULL, WATCHDOG_ERROR);
437 }
438 }
439
440 /**
441 * Implementation of hook_nodeapi().
442 */
443 function apachesolr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
444 switch ($op) {
445 case 'delete':
446 _apachesolr_nodeapi_delete($node);
447 break;
448 case 'insert':
449 // Make sure no node ends up with a timestamp that's in the future
450 // by using time() rather than the node's changed or created timestamp.
451 db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid, $node->status, time());
452 break;
453 case 'update':
454 _apachesolr_nodeapi_update($node);
455 break;
456 }
457 }
458
459 /**
460 * Helper function for hook_nodeapi() and hook_cron().
461 */
462 function _apachesolr_nodeapi_delete($node, $set_message = TRUE) {
463 if (apachesolr_delete_node_from_index($node)) {
464 // There was no exception, so delete from the table.
465 db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid);
466 if ($set_message && user_access('administer site configuration') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
467 apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.');
468 }
469 }
470 }
471
472 /**
473 * Helper function for hook_nodeapi() and hook_cron().
474 */
475 function _apachesolr_nodeapi_update($node, $set_message = TRUE) {
476 // Check if the node has gone from published to unpublished.
477 if (!$node->status && db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid))) {
478 if (apachesolr_delete_node_from_index($node)) {
479 // There was no exception, so update the table.
480 db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
481 if ($set_message && user_access('administer site configuration') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
482 apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.');
483 }
484 }
485 }
486 else {
487 db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
488 }
489 }
490
491 /**
492 * Call drupal_set_message() with the text.
493 *
494 * The text is translated with t() and substituted using Solr stats.
495 */
496 function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE) {
497 try {
498 $solr = apachesolr_get_solr();
499 $stats_summary = $solr->getStatsSummary();
500 drupal_set_message(t($text, $stats_summary), $type, FALSE);
501 }
502 catch (Exception $e) {
503 watchdog('apachesolr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
504 }
505 }
506
507 /**
508 * Return the enabled facets from the specified block array.
509 *
510 * @param $module
511 * The module (optional).
512 * @return
513 * An array consisting info for facets that have been enabled
514 * for the specified module, or all enabled facets.
515 */
516 function apachesolr_get_enabled_facets($module = NULL) {
517 $enabled = variable_get('apachesolr_enabled_facets', array());
518 if (isset($module)) {
519 return isset($enabled[$module]) ? $enabled[$module] : array();
520 }
521 return $enabled;
522 }
523
524 /**
525 * Implementation of hook_block().
526 */
527 function apachesolr_block($op = 'list', $delta = 0, $edit = array()) {
528 static $access;
529
530 switch ($op) {
531 case 'list':
532 // Get all of the moreLikeThis blocks that the user has created
533 $blocks = apachesolr_mlt_list_blocks();
534 // Add the sort block.
535 $blocks['sort'] = array(
536 'info' => t('Apache Solr Core: Sorting'),
537 'cache' => BLOCK_CACHE_PER_PAGE,
538 );
539 return $blocks;
540
541 case 'view':
542 if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) {
543 $suggestions = array();
544 // Determine whether the user can view the current node.
545 if (!isset($access)) {
546 $access = node_access('view', $node);
547 }
548 $block = apachesolr_mlt_load_block($delta);
549 if ($access && $block) {
550 $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid));
551 if (!empty($docs)) {
552 $suggestions['subject'] = check_plain($block['name']);
553 $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs);
554 if (user_access('administer search')) {
555 $suggestions['content'] .= l(t('Configure this block'),'admin/build/block/configure/apachesolr/' . $delta, array('attributes' => array('class' => 'apachesolr-mlt-admin-link')));
556 }
557 }
558 }
559 return $suggestions;
560 }
561 elseif (apachesolr_has_searched() && $delta == 'sort') {
562 // Get the query and response. Without these no blocks make sense.
563 $response = apachesolr_static_response_cache();
564 if (empty($response) || ($response->response->numFound < 2)) {
565 return;
566 }
567
568 $query = apachesolr_current_query();
569 $sorts = $query->get_available_sorts();
570
571 $solrsorts = array();
572 $sort_parameter = isset($_GET['solrsort']) ? check_plain($_GET['solrsort']) : FALSE;
573 foreach (explode(',', $sort_parameter) as $solrsort) {
574 $parts = explode(' ', $solrsort);
575 if (!empty($parts[0]) && !empty($parts[1])) {
576 $solrsorts[$parts[0]] = $parts[1];
577 }
578 }
579
580 $sort_links = array();
581 $path = $query->get_path();
582 $new_query = clone $query;
583 foreach ($sorts as $type => $sort) {
584 $new_sort = isset($solrsorts[$type]) ? $solrsorts[$type] == 'asc' ? 'desc' : 'asc' : $sort['default'];
585 $new_query->set_solrsort($type == "relevancy" ? '' : "{$type} {$new_sort}");
586 $active = isset($solrsorts[$type]) || ($type == "relevancy" && !$solrsorts);
587 $direction = isset($solrsorts[$type]) ? $solrsorts[$type] : '';
588 $sort_links[$type] = array(
589 'name' => $sort['name'],
590 'path' => $path,
591 'options' => array('query' => $new_query->get_url_querystring()),
592 'active' => $active,
593 'direction' => $direction
594 );
595 }
596 // Allow other modules to add or remove sorts.
597 drupal_alter('apachesolr_sort_links', $sort_links);
598 foreach ($sort_links as $type => $link) {
599 $themed_links[$type] = theme('apachesolr_sort_link', $link['name'], $link['path'], $link['options'], $link['active'], $link['direction']);
600 }
601 return array('subject' => t('Sort by'),
602 'content' => theme('apachesolr_sort_list', $themed_links));
603 }
604 break;
605 case 'configure':
606 if ($delta != 'sort') {
607 require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
608 return apachesolr_mlt_block_form($delta);
609 }
610 break;
611 case 'save':
612 if ($delta != 'sort') {
613 require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
614 apachesolr_mlt_save_block($edit, $delta);
615 }
616 break;
617 }
618 }
619
620 /**
621 * Helper function for displaying a facet block.
622 */
623 function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
624 if (!empty($response->facet_counts->facet_fields->$facet_field)) {
625 $contains_active = FALSE;
626 $items = array();
627 foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) {
628 $sortpre = 1000000 - $count;
629 $options = array();
630 $exclude = FALSE;
631 // Solr sends this back if it's empty.
632 if ($facet == '_empty_') {
633 $exclude = TRUE;
634 $facet = '[* TO *]';
635 $facet_text = theme('placeholder', t('Missing this field'));
636 $options['html'] = TRUE;
637 // Put this just below any active facets.
638 // '-' sorts before all numbers, but after '*'.
639 $sortpre = '-';
640 }
641 else {
642 $facet_text = $facet;
643 }
644
645 if ($facet_callback && function_exists($facet_callback)) {
646 $facet_text = $facet_callback($facet, $options);
647 }
648 $unclick_link = '';
649 $active = FALSE;
650 $new_query = clone $query;
651 if ($query->has_filter($facet_field, $facet)) {
652 $contains_active = $active = TRUE;
653 // '*' sorts before all numbers.
654 $sortpre = '*';
655 $new_query->remove_filter($facet_field, $facet);
656 $options['query'] = $new_query->get_url_querystring();
657 $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options);
658 }
659 else {
660 $new_query->add_filter($facet_field, $facet, $exclude);
661 $options['query'] = $new_query->get_url_querystring();
662 $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, $active, $response->response->numFound);
663 }
664 if ($count || $active) {
665 $items[$sortpre . '*' . $facet_text] = $link;
666 }
667 }
668 // Unless a facet is active only display 2 or more.
669 if ($items && ($response->response->numFound > 1 || $contains_active)) {
670 ksort($items, SORT_STRING);
671 // Get information needed by the rest of the blocks about limits.
672 $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
673 $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
674 $output = theme('apachesolr_facet_list', $items, $limit);
675 return array('subject' => $filter_by, 'content' => $output);
676 }
677 }
678 return NULL;
679 }
680
681 /**
682 * Helper function for displaying a date facet block.
683 *
684 * TODO: Refactor with apachesolr_facet_block().
685 */
686 function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
687 $items = array();
688
689 $new_query = clone $query;
690 foreach (array_reverse($new_query->get_filters($facet_field)) as $filter) {
691 $options = array();
692 // Iteratively remove the date facets.
693 $new_query->remove_filter($facet_field, $filter['#value']);
694 if ($facet_callback && function_exists($facet_callback)) {
695 $facet_text = $facet_callback($filter['#start'], $options);
696 }
697 else {
698 $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']);
699 }
700 $options['query'] = $new_query->get_url_querystring();
701 array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options));
702 }
703 // Add links for additional date filters.
704 if (!empty($response->facet_counts->facet_dates->$facet_field)) {
705 $field = clone $response->facet_counts->facet_dates->$facet_field;
706
707 $end = $field->end;
708 unset($field->end);
709
710 $gap = $field->gap;
711 unset($field->gap);
712
713 // Treat each date facet as a range start, and use the next date
714 // facet as range end. Use 'end' for the final end.
715 $range_end = array();
716 foreach ($field as $facet => $count) {
717 if (isset($prev_facet)) {
718 $range_end[$prev_facet] = $facet;
719 }
720 $prev_facet = $facet;
721 }
722 $range_end[$prev_facet] = $end;
723
724 foreach ($field as $facet => $count) {
725 $options = array();
726 // Solr sends this back if it's empty.
727 if ($facet == '_empty_' || $count == 0) {
728 continue;
729 }
730 if ($facet_callback && function_exists($facet_callback)) {
731 $facet_text = $facet_callback($facet, $options);
732 }
733 else {
734 $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet);
735 }
736 $new_query = clone $query;
737 $new_query->add_filter($facet_field, '['. $facet .' TO '. $range_end[$facet] .']');
738 $options['query'] = $new_query->get_url_querystring();
739 $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound);
740 }
741 }
742 if (count($items) > 0) {
743 // Get information needed by the rest of the blocks about limits.
744 $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
745 $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
746 $output = theme('apachesolr_facet_list', $items, $limit);
747 return array('subject' => $filter_by, 'content' => $output);
748 }
749 return NULL;
750 }
751
752 /**
753 * Determine the gap in a date range query filter that we generated.
754 *
755 * This function assumes that the start and end dates are the
756 * beginning and end of a single period: 1 year, month, day, hour,
757 * minute, or second (all date range query filters we generate meet
758 * this criteria). So, if the seconds are different, it is a second
759 * gap. If the seconds are the same (incidentally, they will also be
760 * 0) but the minutes are different, it is a minute gap. If the
761 * minutes are the same but hours are different, it's an hour gap.
762 * etc.
763 *
764 * @param $start
765 * Start date as an ISO date string.
766 * @param $end
767 * End date as an ISO date string.
768 * @return
769 * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND.
770 */
771 function apachesolr_date_find_query_gap($start_iso, $end_iso) {
772 $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1);
773 $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@';
774 if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) {
775 foreach ($gaps as $gap => $idx) {
776 if ($start[$idx] != $end[$idx]) {
777 return $gap;
778 }
779 }
780 }
781 // can't tell
782 return 'YEAR';
783 }
784
785 /**
786 * Format an ISO date string based on the gap used to generate it.
787 *
788 * This function assumes that gaps less than one day will be displayed
789 * in a search context in which a larger containing gap including a
790 * day is already displayed. So, HOUR, MINUTE, and SECOND gaps only
791 * display time information, without date.
792 *
793 * @param $gap
794 * A gap.
795 * @param $iso
796 * An ISO date string.
797 * @return
798 * A gap-appropriate formatted date.
799 */
800 function apachesolr_date_format_iso_by_gap($gap, $iso) {
801 // TODO: If we assume that multiple search queries are formatted in
802 // order, we could store a static list of all gaps we've formatted.
803 // Then, if we format an HOUR, MINUTE, or SECOND without previously
804 // having formatted a DAY or later, we could include date
805 // information. However, we'd need to do that per-field and I'm not
806 // our callers always have field information handy.
807 $unix = strtotime($iso);
808 if ($unix > 0) {
809 switch ($gap) {
810 case 'YEAR':
811 return gmdate('Y', $unix);
812 case 'MONTH':
813 return gmdate('F Y', $unix);
814 case 'DAY':
815 return gmdate('F j, Y', $unix);
816 case 'HOUR':
817 return gmdate('g A', $unix);
818 case 'MINUTE':
819 return gmdate('g:i A', $unix);
820 case 'SECOND':
821 return gmdate('g:i:s A', $unix);
822 }
823 }
824
825 return $iso;
826 }
827
828 /**
829 * Format the beginning of a date range query filter that we
830 * generated.
831 *
832 * @param $start_iso
833 * The start date.
834 * @param $end_iso
835 * The end date.
836 * @return
837 * A display string reprepsenting the date range, such as "January
838 * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z"
839 */
840 function apachesolr_date_format_range($start_iso, $end_iso) {
841 $gap = apachesolr_date_find_query_gap($start_iso, $end_iso);
842 return apachesolr_date_format_iso_by_gap($gap, $start_iso);
843 }
844
845 /**
846 * Determine the best search gap to use for an arbitrary date range.
847 *
848 * Generally, we the maximum gap that fits between the start and end
849 * date. If they are more than a year apart, 1 year; if they are more
850 * than a month apart, 1 month; etc.
851 *
852 * This function uses Unix timestamps for its computation and so is
853 * not useful for dates outside that range.
854 *
855 * @param $start
856 * Start date as an ISO date string.
857 * @param $end
858 * End date as an ISO date string.
859 * @return
860 * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far
861 * apart $start and $end are.
862 */
863 function apachesolr_date_determine_gap($start, $end) {
864 $start = strtotime($start);
865 $end = strtotime($end);
866
867 if ($end - $start >= 86400*365) {
868 return 'YEAR';
869 }
870
871 if (date('m', $start) != date('m', $end)) {
872 return 'MONTH';
873 }
874
875 if ($end - $start > 86400) {
876 return 'DAY';
877 }
878 // For now, HOUR is a reasonable smallest gap.
879 return 'HOUR';
880 }
881
882 /**
883 * Return the next smaller date gap.
884 *
885 * @param $gap
886 * A gap.
887 * @return
888 * The next smaller gap, or NULL if there is no smaller gap.
889 */
890 function apachesolr_date_gap_drilldown($gap) {
891 $drill = array(
892 'YEAR' => 'MONTH',
893 'MONTH' => 'DAY',
894 'DAY' => 'HOUR',
895 // For now, HOUR is a reasonable smallest gap.
896 // 'HOUR' => 'MINUTE',
897 );
898 return isset($drill[$gap]) ? $drill[$gap] : NULL;
899 }
900
901 /**
902 * Used by the 'configure' $op of hook_block so that modules can generically set
903 * facet limits on their blocks.
904 */
905 function apachesolr_facetcount_form($module, $delta) {
906 $initial = variable_get('apachesolr_facet_query_initial_limits', array());
907 $limits = variable_get('apachesolr_facet_query_limits', array());
908 $facet_missing = variable_get('apachesolr_facet_missing', array());
909
910 $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3));
911
912 $form['apachesolr_facet_query_initial_limit'] = array(
913 '#type' => 'select',
914 '#title' => t('Initial filter links'),
915 '#options' => $limit,
916 '#description' => t('The initial number of filter links to show in this block.'),
917 '#default_value' => isset($initial[$module][$delta]) ? $initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10),
918 );
919 $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3));
920 $form['apachesolr_facet_query_limit'] = array(
921 '#type' => 'select',
922 '#title' => t('Maximum filter links'),
923 '#options' => $limit,
924 '#description' => t('The maximum number of filter links to show in this block.'),
925 '#default_value' => isset($limits[$module][$delta]) ? $limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20),
926 );
927 $form['apachesolr_facet_missing'] = array(
928 '#type' => 'radios',
929 '#title' => t('Include a facet for missing'),
930 '#options' => array(0 => t('No'), 1 => t('Yes')),
931 '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'),
932 '#default_value' => isset($facet_missing[$module][$delta]) ? $facet_missing[$module][$delta] : 0,
933 );
934 return $form;
935 }
936
937 /**
938 * Used by the 'save' $op of hook_block so that modules can generically set
939 * facet limits on their blocks.
940 */
941 function apachesolr_facetcount_save($edit) {
942 // Save query limits
943 $module = $edit['module'];
944 $delta = $edit['delta'];
945 $limits = variable_get('apachesolr_facet_query_limits', array());
946 $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit'];
947 variable_set('apachesolr_facet_query_limits', $limits);
948 $initial = variable_get('apachesolr_facet_query_initial_limits', array());
949 $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit'];
950 variable_set('apachesolr_facet_query_initial_limits', $initial);
951 $facet_missing = variable_get('apachesolr_facet_missing', array());
952 $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing'];
953 variable_set('apachesolr_facet_missing', $facet_missing);
954 }
955
956 /**
957 * This hook allows modules to modify the query and params objects.
958 *
959 * Example:
960 *
961 * function my_module_apachesolr_modify_query(&$query, &$params) {
962 * // I only want to see articles by the admin!
963 * $query->add_filter("uid", 1);
964 *
965 * }
966 */
967 function apachesolr_modify_query(&$query, &$params, $caller) {
968
969 foreach (module_implements('apachesolr_modify_query') as $module) {
970 $function_name = $module . '_apachesolr_modify_query';
971 $function_name($query, $params, $caller);
972 }
973 // Add array of fq parameters.
974 if ($query && ($fq = $query->get_fq())) {
975 $params['fq'] = $fq;
976 }
977 }
978
979 /**
980 * Semaphore that indicates whether a search has been done. Blocks use this
981 * later to decide whether they should load or not.
982 *
983 * @param $searched
984 * A boolean indicating whether a search has been executed.
985 *
986 * @return
987 * TRUE if a search has been executed.
988 * FALSE otherwise.
989 */
990 function apachesolr_has_searched($searched = NULL) {
991 static $_searched = FALSE;
992 if (is_bool($searched)) {
993 $_searched = $searched;
994 }
995 return $_searched;
996 }
997
998 /**
999 * Factory method for solr singleton object. Structure allows for an arbitrary
1000 * number of solr objects to be used based on the host, port, path combination.
1001 * Get an instance like this:
1002 * $solr = apachesolr_get_solr();
1003 */
1004 function apachesolr_get_solr($host = NULL, $port = NULL, $path = NULL) {
1005 static $solr_cache;
1006
1007 if (empty($host)) {
1008 $host = variable_get('apachesolr_host', 'localhost');
1009 }
1010 if (empty($port)) {
1011 $port = variable_get('apachesolr_port', '8983');
1012 }
1013 if (empty($path)) {
1014 $path = variable_get('apachesolr_path', '/solr');
1015 }
1016
1017 if (empty($solr_cache[$host][$port][$path])) {
1018 list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service'));
1019 include_once(drupal_get_path('module', $module) .'/'. $filepath);
1020 try {
1021 $solr_cache[$host][$port][$path] = new $class($host, $port, $path);
1022 }
1023 catch (Exception $e) {
1024 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
1025 return;
1026 }
1027 }
1028 return $solr_cache[$host][$port][$path];
1029 }
1030
1031 /**
1032 * It is important to hold on to the Solr response object for the duration of the
1033 * page request so that we can use it for things like building facet blocks.
1034 */
1035 function apachesolr_static_response_cache($response = NULL) {
1036 static $_response;
1037
1038 if (!empty($response)) {
1039 $_response = clone $response;
1040 }
1041 return $_response;
1042 }
1043
1044 /**
1045 * Factory function for query objects.
1046 *
1047 * @param $keys
1048 * The string that a user would type into the search box. Suitable input
1049 * may come from search_get_keys().
1050 *
1051 * @param $filters
1052 * Key and value pairs that are applied as a filter query.
1053 *
1054 * @param $solrsort
1055 * Visible string telling solr how to sort.
1056 *
1057 * @param $base_path
1058 * The search base path (without the keywords) for this query.
1059 */
1060 function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '') {
1061 list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query'));
1062 include_once drupal_get_path('module', $module) .'/'. $class .'.php';
1063
1064 try {
1065 $query = new $class(apachesolr_get_solr(), $keys, $filters, $solrsort, $base_path);
1066 }
1067 catch (Exception $e) {
1068 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
1069 $query = NULL;
1070 }
1071
1072 return $query;
1073 }
1074
1075 /**
1076 * Static getter/setter for the current query
1077 */
1078 function apachesolr_current_query($query = NULL) {
1079 static $saved_query = NULL;
1080 if (is_object($query)) {
1081 $saved_query = clone $query;
1082 }
1083
1084 return empty($saved_query) ? $saved_query : clone $saved_query;
1085 }
1086
1087 /**
1088 * array('index_type' => 'integer',
1089 * 'multiple' => TRUE,
1090 * 'name' => 'fieldname',
1091 * ),
1092 */
1093 function apachesolr_index_key($field) {
1094 switch ($field['index_type']) {
1095 case 'text':
1096 $type_prefix = 't';
1097 break;
1098 case 'string':
1099 $type_prefix = 's';
1100 break;
1101 case 'integer':
1102 $type_prefix = 'i';
1103 break;
1104 case 'sint':
1105 $type_prefix = 'si';
1106 break;
1107 case 'double':
1108 $type_prefix = 'p'; // reserve d for date
1109 break;
1110 case 'boolean':
1111 $type_prefix = 'b';
1112 break;
1113 case 'date':
1114 $type_prefix = 'd';
1115 break;
1116 case 'float':
1117 $type_prefix = 'f';
1118 break;
1119 default:
1120 $type_prefix = 's';
1121 }
1122 $sm = $field['multiple'] ? 'm_' : 's_';
1123 return $type_prefix . $sm . $field['name'];
1124 }
1125
1126 /**
1127 * Try to map a schema field name to a human-readable description.
1128 */
1129 function apachesolr_field_name_map($field_name) {
1130 require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
1131 return _apachesolr_field_name_map($field_name);
1132 }
1133
1134 /**
1135 * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
1136 */
1137 function apachesolr_cck_fields() {
1138 static $fields;
1139
1140 if (is_null($fields)) {
1141 $fields = array();
1142 // If CCK isn't enabled, do nothing.
1143 if (module_exists('content')) {
1144 // A single default mapping for all text fields.
1145 $mappings['text'] = array(
1146 'optionwidgets_select' => array('callback' => '', 'index_type' => 'string'),
1147 'optionwidgets_buttons' => array('callback' => '', 'index_type' => 'string')
1148