5 * Integration with the Apache Solr search application.
8 define('APACHESOLR_READ_WRITE', 0);
9 define('APACHESOLR_READ_ONLY', 1);
10 define('APACHESOLR_API_VERSION', '1.0');
13 * Implementation of hook_init().
15 * PHP 5.1 compatability code.
17 function apachesolr_init() {
18 if (!function_exists('json_decode')) {
19 // Zend files include other files.
20 set_include_path(dirname(__FILE__
) . PATH_SEPARATOR .
get_include_path());
21 require_once
'Zend/Json/Decoder.php';
22 require_once
'Zend/Json/Encoder.php';
25 * Substitute for missing PHP built-in functions.
27 function json_decode($string, $assoc = FALSE
) {
29 $objectDecodeType = Zend_Json
::TYPE_ARRAY
;
32 $objectDecodeType = Zend_Json
::TYPE_OBJECT
;
34 return Zend_Json_Decoder
::decode($string, $objectDecodeType);
37 function json_encode($data) {
38 return Zend_Json_Encoder
::encode($data, $objectDecodeType);
44 * Implementation of hook_menu().
46 function apachesolr_menu() {
48 $items['admin/settings/apachesolr'] = array(
49 'title' => 'Apache Solr',
50 'description' => 'Administer Apache Solr.',
51 'page callback' => 'drupal_get_form',
52 'page arguments' => array('apachesolr_settings'),
53 'access callback' => 'user_access',
54 'access arguments' => array('administer search'),
55 'file' => 'apachesolr.admin.inc',
57 $items['admin/settings/apachesolr/settings'] = array(
58 'title' => 'Settings',
60 'access arguments' => array('administer search'),
61 'file' => 'apachesolr.admin.inc',
62 'type' => MENU_DEFAULT_LOCAL_TASK
,
64 $items['admin/settings/apachesolr/enabled-filters'] = array(
65 'title' => 'Enabled filters',
66 'page callback' => 'drupal_get_form',
67 'page arguments' => array('apachesolr_enabled_facets_form'),
69 'access arguments' => array('administer search'),
70 'file' => 'apachesolr.admin.inc',
71 'type' => MENU_LOCAL_TASK
,
73 $items['admin/settings/apachesolr/index'] = array(
74 'title' => 'Search index',
75 'page callback' => 'apachesolr_index_page',
76 'access arguments' => array('administer search'),
78 'file' => 'apachesolr.admin.inc',
79 'type' => MENU_LOCAL_TASK
,
81 $items['admin/settings/apachesolr/index/clear/confirm'] = array(
82 'title' => 'Confirm the re-indexing of all content',
83 'page callback' => 'drupal_get_form',
84 'page arguments' => array('apachesolr_clear_index_confirm'),
85 'access arguments' => array('administer search'),
86 'file' => 'apachesolr.admin.inc',
87 'type' => MENU_CALLBACK
,
89 $items['admin/settings/apachesolr/index/delete/confirm'] = array(
90 'title' => 'Confirm index deletion',
91 'page callback' => 'drupal_get_form',
92 'page arguments' => array('apachesolr_delete_index_confirm'),
93 'access arguments' => array('administer search'),
94 'file' => 'apachesolr.admin.inc',
95 'type' => MENU_CALLBACK
,
97 $items['admin/reports/apachesolr'] = array(
98 'title' => 'Apache Solr search index',
99 'page callback' => 'apachesolr_index_report',
100 'access arguments' => array('access site reports'),
101 'file' => 'apachesolr.admin.inc',
103 $items['admin/reports/apachesolr/index'] = array(
104 'title' => 'Search index',
105 'file' => 'apachesolr.admin.inc',
106 'type' => MENU_DEFAULT_LOCAL_TASK
,
108 $items['admin/reports/apachesolr/conf'] = array(
109 'title' => 'Configuration files',
110 'page callback' => 'apachesolr_config_files_overview',
111 'access arguments' => array('access site reports'),
112 'file' => 'apachesolr.admin.inc',
114 'type' => MENU_LOCAL_TASK
,
116 $items['admin/reports/apachesolr/conf/%'] = array(
117 'title' => 'Configuration file',
118 'page callback' => 'apachesolr_config_file',
119 'page arguments' => array(4),
120 'access arguments' => array('access site reports'),
121 'file' => 'apachesolr.admin.inc',
122 'type' => MENU_CALLBACK
,
124 $items['admin/settings/apachesolr/mlt/add_block'] = array(
125 'page callback' => 'drupal_get_form',
126 'page arguments' => array('apachesolr_mlt_add_block_form'),
127 'access arguments' => array('administer search'),
128 'file' => 'apachesolr.admin.inc',
129 'type' => MENU_CALLBACK
,
131 $items['admin/settings/apachesolr/mlt/delete_block/%'] = array(
132 'page callback' => 'drupal_get_form',
133 'page arguments' => array('apachesolr_mlt_delete_block_form', 5),
134 'access arguments' => array('administer search'),
135 'file' => 'apachesolr.admin.inc',
136 'type' => MENU_CALLBACK
,
142 * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.)
143 * Depending on the admin settings, possibly redirect to Drupal's core search.
145 * @param $search_name
146 * The name of the search implementation.
148 * @param $querystring
149 * The search query that was issued at the time of failure.
151 function apachesolr_failure($search_name, $querystring) {
152 $fail_rule = variable_get('apachesolr_failure', 'show_error');
154 switch ($fail_rule) {
156 drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error');
158 case
'show_drupal_results':
159 drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name)));
160 drupal_goto('search/node/' .
drupal_urlencode($querystring));
162 case
'show_no_results':
168 * Like $site_key in _update_refresh() - returns a site-specific hash.
170 function apachesolr_site_hash() {
171 if (!($hash = variable_get('apachesolr_site_hash', FALSE
))) {
173 // Set a random 6 digit base-36 number as the hash.
174 $hash = substr(base_convert(sha1(uniqid($base_url, TRUE
)), 16, 36), 0, 6);
175 variable_set('apachesolr_site_hash', $hash);
181 * Generate a unique ID for an entity being indexed.
184 * An id number (or string) unique to this site, such as a node ID.
186 * A string like 'node', 'file', 'user', or some other Drupal object type.
189 * A string combining the parameters with the site hash.
191 function apachesolr_document_id($id, $entity = 'node') {
192 return apachesolr_site_hash() .
"/$entity/" .
$id;
196 * Implementation of hook_user().
198 * Mark nodes as needing re-indexing if the author name changes.
200 function apachesolr_user($op, &$edit, &$account) {
203 if (isset($edit['name']) && $account->name
!= $edit['name']) {
204 switch ($GLOBALS['db_type']) {
207 db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.uid = %d", time(), $account->uid
);
210 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid
);
219 * Implementation of hook_taxonomy().
221 * Mark nodes as needing re-indexing if a term name changes.
223 function apachesolr_taxonomy($op, $type, $edit) {
224 if ($type == 'term' && ($op == 'update')) {
225 switch ($GLOBALS['db_type']) {
228 db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {term_node} tn ON asn.nid = tn.nid SET asn.changed = %d WHERE tn.tid = %d", time(), $edit['tid']);
231 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']);
235 // TODO: the rest, such as term deletion.
239 * Implementation of hook_comment().
241 * Mark nodes as needing re-indexing if comments are added or changed.
242 * Like search_comment().
244 function apachesolr_comment($edit, $op) {
245 $edit = (array) $edit;
247 // Reindex the node when comments are added or changed
253 // TODO: do we want to skip this if we are excluding comments
254 // from the index for this node type?
255 apachesolr_mark_node($edit['nid']);
261 * Mark one node as needing re-indexing.
263 function apachesolr_mark_node($nid) {
264 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid);
268 * Implementation of hook_node_type().
270 * Mark nodes as needing re-indexing if a node type name changes.
272 function apachesolr_node_type($op, $info) {
273 if ($op != 'delete' && !empty($info->old_type
) && $info->old_type
!= $info->type
) {
274 // We cannot be sure we are going before or after node module.
275 switch ($GLOBALS['db_type']) {
278 db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE (n.type = '%s' OR n.type = '%s')", time(), $info->old_type
, $info->type
);
281 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
);
288 * Helper function for modules implmenting hook_search's 'status' op.
290 function apachesolr_index_status($namespace) {
291 list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
292 $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ".
$join_sql .
"WHERE asn.status = 1 " .
$exclude_sql, $excluded_types));
293 $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));
294 return array('remaining' => $remaining, 'total' => $total);
298 * Returns last changed and last nid for an indexing namespace.
300 function apachesolr_get_last_index($namespace) {
301 $stored = variable_get('apachesolr_index_last', array());
302 return isset($stored[$namespace]) ?
$stored[$namespace] : array('last_change' => 0, 'last_nid' => 0);
306 * Clear a specific namespace's last changed and nid, or clear all.
308 function apachesolr_clear_last_index($namespace = '') {
310 $stored = variable_get('apachesolr_index_last', array());
311 unset($stored[$namespace]);
312 variable_set('apachesolr_index_last', $stored);
315 variable_del('apachesolr_index_last');
320 * Truncate and rebuild the apachesolr_search_node table, reset the apachesolr_index_last variable.
321 * This is the most complete way to force reindexing, or to build the indexing table for the
325 * A single content type to be reindexed, leaving the others unaltered.
327 function apachesolr_rebuild_index_table($type = NULL
) {
330 switch ($GLOBALS['db_type']) {
333 db_query("DELETE FROM {apachesolr_search_node} USING {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid WHERE n.type = '%s'", $type);
336 db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type);
340 db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
341 SELECT n.nid, n.status, %d AS changed
342 FROM {node} n WHERE n.type = '%s'", time(), $type);
345 db_query("DELETE FROM {apachesolr_search_node}");
347 if (module_exists('comment')) {
348 // If comment module is enabled, use last_comment_timestamp as well.
349 db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
350 SELECT n.nid, n.status, GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0)) AS changed
352 LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid");
355 db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
356 SELECT n.nid, n.status, GREATEST(n.created, n.changed) AS changed
359 // Make sure no nodes end up with a timestamp that's in the future.
361 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE changed > %d", $time, $time);
362 apachesolr_clear_last_index();
367 function _apachesolr_exclude_types($namespace) {
368 extract(apachesolr_get_last_index($namespace));
369 $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace);
370 $args = array($last_change, $last_change, $last_nid);
373 if ($excluded_types) {
374 $excluded_types = array_unique($excluded_types);
375 $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid ";
376 $exclude_sql = "AND n.type NOT IN(".
db_placeholders($excluded_types, 'varchar') .
") ";
377 $args = array_merge($args, $excluded_types);
379 return array($excluded_types, $args, $join_sql, $exclude_sql);
383 * Returns an array of rows from a query based on an indexing namespace.
385 function apachesolr_get_nodes_to_index($namespace, $limit) {
387 if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE
) == APACHESOLR_READ_ONLY
) {
390 list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
391 $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);
392 while ($row = db_fetch_object($result)) {
399 * Function to handle the indexing of nodes.
401 * The calling function must supply a name space or track/store
402 * the timestamp and nid returned.
403 * Returns FALSE if no nodes were indexed (none found or error).
405 function apachesolr_index_nodes($rows, $namespace = '', $callback = 'apachesolr_add_node_document') {
412 // Get the $solr object
413 $solr = apachesolr_get_solr();
414 // If there is no server available, don't continue.
415 if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) {
416 throw new
Exception(t('No Solr instance available during indexing.'));
419 catch (Exception
$e) {
420 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
423 module_load_include('inc', 'apachesolr', 'apachesolr.index');
424 $documents = array();
425 $old_position = apachesolr_get_last_index($namespace);
426 $position = $old_position;
428 // Always build the content for the index as an anonynmous user.
430 session_save_session(FALSE
);
432 $user = drupal_anonymous_user();
434 foreach ($rows as
$row) {
436 $callback($documents, $row->nid
, $namespace);
437 // Variables to track the last item changed.
438 $position['last_change'] = $row->changed
;
439 $position['last_nid'] = $row->nid
;
441 catch (Exception
$e) {
442 // Something bad happened - log the error.
443 watchdog('Apache Solr', 'Error constructing documents to index: <br /> !message', array('!message' => "Node ID: {$row->nid}<br />" .
nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR
);
448 session_save_session(TRUE
);
450 if (count($documents)) {
452 watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents)));
453 // Chunk the adds by 20s
454 $docs_chunk = array_chunk($documents, 20);
455 foreach ($docs_chunk as
$docs) {
456 $solr->addDocuments($docs);
458 // Set the timestamp to indicate an index update.
459 apachesolr_index_updated(time());
461 catch (Exception
$e) {
464 foreach ($docs as
$doc) {
468 watchdog('Apache Solr', 'Indexing failed on one of the following nodes: @nids <br /> !message', array(
469 '@nids' => implode(', ', $nids),
470 '!message' => nl2br(strip_tags($e->getMessage())),
476 // Save the new position in case it changed.
477 if ($namespace && $position != $old_position) {
478 $stored = variable_get('apachesolr_index_last', array());
479 $stored[$namespace] = $position;
480 variable_set('apachesolr_index_last', $stored);
487 * Convert date from timestamp into ISO 8601 format.
488 * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
490 function apachesolr_date_iso($date_timestamp) {
491 return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp);
494 function apachesolr_delete_node_from_index($node) {
495 static
$failed = FALSE
;
500 $solr = apachesolr_get_solr();
501 $solr->deleteById(apachesolr_document_id($node->nid
));
502 apachesolr_index_updated(time());
505 catch (Exception
$e) {
506 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
507 // Don't keep trying queries if they are failing.
514 * Helper function to keep track of when the index has been updated.
516 function apachesolr_index_updated($updated = NULL
) {
517 if (isset($updated)) {
519 variable_set('apachesolr_index_updated', (int) $updated);
522 variable_del('apachesolr_index_updated');
525 return variable_get('apachesolr_index_updated', 0);
529 * Implementation of hook_cron().
531 function apachesolr_cron() {
533 // Indexes in read-only mode do not change the index, so will not update, delete, or optimize during cron.
534 if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE
) == APACHESOLR_READ_ONLY
) {
538 // Mass update and delete functions are in the include file.
539 module_load_include('inc', 'apachesolr', 'apachesolr.index');
540 apachesolr_cron_check_node_table();
542 $solr = apachesolr_get_solr();
543 // Optimize the index (by default once a day).
544 $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24);
545 $last = variable_get('apachesolr_last_optimize', 0);
547 if ($optimize_interval && ($time - $last > $optimize_interval)) {
548 $solr->optimize(FALSE
, FALSE
);
549 variable_set('apachesolr_last_optimize', $time);
550 apachesolr_index_updated($time);
553 // Only clear the cache if the index changed.
554 // TODO: clear on some schedule if running multi-site.
555 $updated = apachesolr_index_updated();
558 // Re-populate the luke cache.
560 // TODO: an admin interface for setting this. Assume for now 5 minutes.
561 if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) {
562 // Clear the updated flag.
563 apachesolr_index_updated(FALSE
);
567 catch (Exception
$e) {
568 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) .
' in apachesolr_cron', NULL
, WATCHDOG_ERROR
);
573 * Implementation of hook_flush_caches().
575 function apachesolr_flush_caches() {
576 return array('cache_apachesolr');
580 * A wrapper for cache_clear_all to be used as a submit handler on forms that
581 * require clearing Luke cache etc.
583 function apachesolr_clear_cache() {
585 $solr = apachesolr_get_solr();
588 catch (Exception
$e) {
589 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
590 drupal_set_message(nl2br(check_plain($e->getMessage())), 'warning');
595 * Implementation of hook_nodeapi().
597 function apachesolr_nodeapi(&$node, $op, $a3 = NULL
, $a4 = NULL
) {
600 _apachesolr_nodeapi_delete($node);
603 // Make sure no node ends up with a timestamp that's in the future
604 // by using time() rather than the node's changed or created timestamp.
605 db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid
, $node->status
, time());
608 _apachesolr_nodeapi_update($node);
614 * Helper function for hook_nodeapi().
616 function _apachesolr_nodeapi_delete($node, $set_message = TRUE
) {
617 if (apachesolr_delete_node_from_index($node)) {
618 // There was no exception, so delete from the table.
619 db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid
);
620 if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
621 apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.');
627 * Helper function for hook_nodeapi().
629 function _apachesolr_nodeapi_update($node, $set_message = TRUE
) {
630 // Check if the node has gone from published to unpublished.
631 if (!$node->status
&& db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid
))) {
632 if (apachesolr_delete_node_from_index($node)) {
633 // There was no exception, so update the table.
634 db_query('UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d', time(), $node->status
, $node->nid
);
635 if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
636 apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.');
641 db_query('UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d', time(), $node->status
, $node->nid
);
646 * Implementation of hook_content_fieldapi().
648 function apachesolr_content_fieldapi($op, $field) {
650 case
'delete instance':
651 apachesolr_mark_node_type($field['type_name']);
653 case
'update instance':
654 // Get the previous value from the table.
655 $previous = content_field_instance_read(array('field_name' => $field['field_name'], 'type_name' => $field['type_name']));
656 $prev_field = array_pop($previous);
657 if ($field['display_settings'][NODE_BUILD_SEARCH_INDEX
]['exclude'] != $prev_field['display_settings'][NODE_BUILD_SEARCH_INDEX
]['exclude']) {
658 apachesolr_mark_node_type($field['type_name']);
660 elseif ($field['multiple'] != $prev_field['multiple']) {
661 apachesolr_mark_node_type($field['type_name']);
668 * Mark all nodes of one type as needing re-indexing.
670 function apachesolr_mark_node_type($type_name) {
671 switch ($GLOBALS['db_type']) {
674 db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.type = '%s'", time(), $type_name);
677 db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", time(), $type_name);
683 * Call drupal_set_message() with the text.
685 * The text is translated with t() and substituted using Solr stats.
687 function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE
) {
689 $solr = apachesolr_get_solr();
690 $stats_summary = $solr->getStatsSummary();
691 drupal_set_message(t($text, $stats_summary), $type, FALSE
);
693 catch (Exception
$e) {
694 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
699 * Return the enabled facets from the specified block array.
702 * The module (optional).
704 * An array consisting of info for facets that have been enabled
705 * for the specified module, or all enabled facets.
707 function apachesolr_get_enabled_facets($module = NULL
) {
708 $enabled = variable_get('apachesolr_enabled_facets', array());
709 if (isset($module)) {
710 return isset($enabled[$module]) ?
$enabled[$module] : array();
716 * Save the enabled facets for all modules.
719 * An array consisting of info for all enabled facets.
721 * The array consisting of info for all enabled facets.
723 function apachesolr_save_enabled_facets($enabled) {
724 variable_set('apachesolr_enabled_facets', $enabled);
729 * Save the enabled facets for one module.
734 * Associative array of $delta => $facet_field pairs. If omitted, all facets
735 * for $module are disabled.
737 * An array consisting of info for all enabled facets.
739 function apachesolr_save_module_facets($module, $facets = array()) {
740 $enabled = variable_get('apachesolr_enabled_facets', array());
741 if (!empty($facets) && is_array($facets)) {
742 $enabled[$module] = $facets;
745 unset($enabled[$module]);
747 variable_set('apachesolr_enabled_facets', $enabled);
752 * Implementation of hook_block().
754 function apachesolr_block($op = 'list', $delta = 0, $edit = array()) {
759 // Get all of the moreLikeThis blocks that the user has created
760 $blocks = apachesolr_mlt_list_blocks();
761 // Add the sort block.
762 $blocks['sort'] = array(
763 'info' => t('Apache Solr Core: Sorting'),
764 'cache' => BLOCK_CACHE_PER_PAGE
,
769 if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) {
770 $suggestions = array();
771 // Determine whether the user can view the current node.
772 if (!isset($access)) {
773 $access = node_access('view', $node);
775 $block = apachesolr_mlt_load_block($delta);
776 if ($access && $block) {
777 $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid
));
779 $suggestions['subject'] = check_plain($block['name']);
780 $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs, $delta);
781 if (user_access('administer search')) {
782 $suggestions['content'] .
= l(t('Configure this block'), 'admin/build/block/configure/apachesolr/' .
$delta, array('attributes' => array('class' => 'apachesolr-mlt-admin-link')));
788 elseif (apachesolr_has_searched() && $delta == 'sort') {
789 // Get the query and response. Without these no blocks make sense.
790 $response = apachesolr_static_response_cache();
791 if (empty($response) || ($response->response
->numFound
< 2)) {
795 $query = apachesolr_current_query();
796 $sorts = $query->get_available_sorts();
798 // Get the current sort as an array.
799 $solrsort = $query->get_solrsort();
801 $sort_links = array();
802 $path = $query->get_path();
803 $new_query = clone
$query;
804 $toggle = array('asc' => 'desc', 'desc' => 'asc');
805 foreach ($sorts as
$name => $sort) {
806 $active = $solrsort['#name'] == $name;
807 if ($name == 'score') {
808 // We only sort by ascending score.
810 $new_direction = 'asc';
813 $direction = $toggle[$solrsort['#direction']];
814 $new_direction = $toggle[$solrsort['#direction']];
818 $new_direction = $sort['default'];
820 $new_query->set_solrsort($name, $new_direction);
821 $sort_links[$name] = array(
822 'title' => $sort['title'],
824 'options' => array('query' => $new_query->get_url_queryvalues()),
826 'direction' => $direction,
829 // Allow other modules to add or remove sorts.
830 drupal_alter('apachesolr_sort_links', $sort_links);
831 if (!empty($sort_links)) {
832 foreach ($sort_links as
$name => $link) {
833 $themed_links[$name] = theme('apachesolr_sort_link', $link['title'], $link['path'], $link['options'], $link['active'], $link['direction']);
836 'subject' => t('Sort by'),
837 'content' => theme('apachesolr_sort_list', $themed_links),
843 if ($delta != 'sort') {
844 require_once(drupal_get_path('module', 'apachesolr') .
'/apachesolr.admin.inc');
845 return apachesolr_mlt_block_form($delta);
849 if ($delta != 'sort') {
850 require_once(drupal_get_path('module', 'apachesolr') .
'/apachesolr.admin.inc');
851 apachesolr_mlt_save_block($edit, $delta);
858 * Helper function for displaying a facet block.
860 function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE
) {
861 if (!empty($response->facet_counts
->facet_fields
->$facet_field)) {
862 $contains_active = FALSE
;
864 foreach ($response->facet_counts
->facet_fields
->$facet_field as
$facet => $count) {
868 // Solr sends this back if it's empty.
869 if ($facet == '_empty_') {
872 $options['html'] = TRUE
;
875 if ($facet_callback && function_exists($facet_callback)) {
876 $facet_text = $facet_callback($facet, $options);
879 $facet_text = theme('placeholder', t('Missing this field'));
882 $facet_text = $facet;
885 $active = $query->has_filter($facet_field, $facet);
888 // '*' sorts before all numbers.
892 // '-' sorts before all numbers, but after '*'.
896 $sortpre = 1000000 - $count;
899 $new_query = clone
$query;
901 $contains_active = TRUE
;
902 $new_query->remove_filter($facet_field, $facet);
903 $options['query'] = $new_query->get_url_queryvalues();
904 $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options);
907 $new_query->add_filter($facet_field, $facet, $exclude);
908 $options['query'] = $new_query->get_url_queryvalues();
909 $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE
, $response->response
->numFound
);
912 if ($count || $active) {
913 $items[$sortpre .
'*' .
$facet_text] = $link;
916 // Unless a facet is active only display 2 or more.
917 if ($items && ($response->response
->numFound
> 1 || $contains_active)) {
918 ksort($items, SORT_STRING
);
919 // Get information needed by the rest of the blocks about limits.
920 $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
921 $limit = isset($initial_limits[$module][$delta]) ?
$initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
922 $output = theme('apachesolr_facet_list', $items, $limit);
923 return array('subject' => $filter_by, 'content' => $output);
930 * Helper function for displaying a date facet block.
932 * TODO: Refactor with apachesolr_facet_block().
934 function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE
) {
937 $new_query = clone
$query;
938 foreach (array_reverse($new_query->get_filters($facet_field)) as
$filter) {
940 // Iteratively remove the date facets.
941 $new_query->remove_filter($facet_field, $filter['#value']);
942 if ($facet_callback && function_exists($facet_callback)) {
943 $facet_text = $facet_callback($filter['#start'], $options);
946 $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']);
948 $options['query'] = $new_query->get_url_queryvalues();
949 array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options));
951 // Add links for additional date filters.
952 if (!empty($response->facet_counts
->facet_dates
->$facet_field)) {
953 $field = clone
$response->facet_counts
->facet_dates
->$facet_field;
961 if (isset($field->start
)) {
962 $start = $field->start
;
963 unset($field->start
);
966 // Treat each date facet as a range start, and use the next date
967 // facet as range end. Use 'end' for the final end.
968 $range_end = array();
969 foreach ($field as
$facet => $count) {
970 if (isset($prev_facet)) {
971 $range_end[$prev_facet] = $facet;
973 $prev_facet = $facet;
975 $range_end[$prev_facet] = $end;
977 foreach ($field as
$facet => $count) {
979 // Solr sends this back if it's empty.
980 if ($facet == '_empty_' || $count == 0) {
983 if ($facet_callback && function_exists($facet_callback)) {
984 $facet_text = $facet_callback($facet, $options);
987 $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet);
989 $new_query = clone
$query;
990 $new_query->add_filter($facet_field, '['.
$facet .
' TO '.
$range_end[$facet] .
']');
991 $options['query'] = $new_query->get_url_queryvalues();
992 $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE
, $response->response
->numFound
);
995 if (count($items) > 0) {
996 // Get information needed by the rest of the blocks about limits.
997 $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
998 $limit = isset($initial_limits[$module][$delta]) ?
$initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
999 $output = theme('apachesolr_facet_list', $items, $limit);
1000 return array('subject' => $filter_by, 'content' => $output);
1006 * Determine the gap in a date range query filter that we generated.
1008 * This function assumes that the start and end dates are the
1009 * beginning and end of a single period: 1 year, month, day, hour,
1010 * minute, or second (all date range query filters we generate meet
1011 * this criteria). So, if the seconds are different, it is a second
1012 * gap. If the seconds are the same (incidentally, they will also be
1013 * 0) but the minutes are different, it is a minute gap. If the
1014 * minutes are the same but hours are different, it's an hour gap.
1018 * Start date as an ISO date string.
1020 * End date as an ISO date string.
1022 * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND.
1024 function apachesolr_date_find_query_gap($start_iso, $end_iso) {
1025 $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1);
1026 $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@';
1027 if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) {
1028 foreach ($gaps as
$gap => $idx) {
1029 if ($start[$idx] != $end[$idx]) {
1039 * Format an ISO date string based on the gap used to generate it.
1041 * This function assumes that gaps less than one day will be displayed
1042 * in a search context in which a larger containing gap including a
1043 * day is already displayed. So, HOUR, MINUTE, and SECOND gaps only
1044 * display time information, without date.
1049 * An ISO date string.
1051 * A gap-appropriate formatted date.
1053 function apachesolr_date_format_iso_by_gap($gap, $iso) {
1054 // TODO: If we assume that multiple search queries are formatted in
1055 // order, we could store a static list of all gaps we've formatted.
1056 // Then, if we format an HOUR, MINUTE, or SECOND without previously
1057 // having formatted a DAY or later, we could include date
1058 // information. However, we'd need to do that per-field and I'm not
1059 // our callers always have field information handy.
1060 $unix = strtotime($iso);
1061 if ($unix !== FALSE
) {
1064 return format_date($unix, 'custom', 'Y', 0);
1066 return format_date($unix, 'custom', 'F Y', 0);
1068 return format_date($unix, 'custom', 'F j, Y', 0);
1070 return format_date($unix, 'custom', 'g A', 0);
1072 return format_date($unix, 'custom', 'g:i A', 0);
1074 return format_date($unix, 'custom', 'g:i:s A', 0);
1082 * Format the beginning of a date range query filter that we
1090 * A display string reprepsenting the date range, such as "January
1091 * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z"
1093 function apachesolr_date_format_range($start_iso, $end_iso) {
1094 $gap = apachesolr_date_find_query_gap($start_iso, $end_iso);
1095 return apachesolr_date_format_iso_by_gap($gap, $start_iso);
1099 * Determine the best search gap to use for an arbitrary date range.
1101 * Generally, we the maximum gap that fits between the start and end
1102 * date. If they are more than a year apart, 1 year; if they are more
1103 * than a month apart, 1 month; etc.
1105 * This function uses Unix timestamps for its computation and so is
1106 * not useful for dates outside that range.
1109 * Start date as an ISO date string.
1111 * End date as an ISO date string.
1113 * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far
1114 * apart $start and $end are.
1116 function apachesolr_date_determine_gap($start, $end) {
1117 $start = strtotime($start);
1118 $end = strtotime($end);
1120 if ($end - $start >= 86400*365) {
1123 if (date('Ym', $start) != date('Ym', $end)) {
1126 if ($end - $start > 86400) {
1133 * Return the next smaller date gap.
1138 * The next smaller gap, or NULL if there is no smaller gap.
1140 function apachesolr_date_gap_drilldown($gap) {
1146 return isset($drill[$gap]) ?
$drill[$gap] : NULL
;
1150 * Used by the 'configure' $op of hook_block so that modules can generically set
1151 * facet limits on their blocks.
1153 function apachesolr_facetcount_form($module, $delta) {
1154 $initial = variable_get('apachesolr_facet_query_initial_limits', array());
1155 $limits = variable_get('apachesolr_facet_query_limits', array());
1156 $children = variable_get('apachesolr_facet_show_children', array());
1157 $facet_missing = variable_get('apachesolr_facet_missing', array());
1159 $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3));
1161 $form['apachesolr_facet_query_initial_limit'] = array(
1162 '#type' => 'select',
1163 '#title' => t('Initial filter links'),
1164 '#options' => $limit,
1165 '#description' => t('The initial number of filter links to show in this block.'),
1166 '#default_value' => isset($initial[$module][$delta]) ?
$initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10),
1168 $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3));
1169 $form['apachesolr_facet_query_limit'] = array(
1170 '#type' => 'select',
1171 '#title' => t('Maximum filter links'),
1172 '#options' => $limit,
1173 '#description' => t('The maximum number of filter links to show in this block.'),
1174 '#default_value' => isset($limits[$module][$delta]) ?
$limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20),
1176 $form['apachesolr_facet_show_children'] = array(
1177 '#type' => 'radios',
1178 '#title' => t('Always show child facets'),
1179 '#options' => array(0 => t('No'), 1 => t('Yes')),
1180 '#description' => t('Show the child facets even if the parent facet is not selected.'),
1181 '#default_value' => isset($children[$module][$delta]) ?
$children[$module][$delta] : 0,
1183 $form['apachesolr_facet_missing'] = array(
1184 '#type' => 'radios',
1185 '#title' => t('Include a facet for missing'),
1186 '#options' => array(0 => t('No'), 1 => t('Yes')),
1187 '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'),
1188 '#default_value' => isset($facet_missing[$module][$delta]) ?
$facet_missing[$module][$delta] : 0,
1194 * Used by the 'save' $op of hook_block so that modules can generically set
1195 * facet limits on their blocks.
1197 function apachesolr_facetcount_save($edit) {
1198 // Save query limits
1199 $module = $edit['module'];
1200 $delta = $edit['delta'];
1201 $limits = variable_get('apachesolr_facet_query_limits', array());
1202 $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit'];
1203 variable_set('apachesolr_facet_query_limits', $limits);
1204 $initial = variable_get('apachesolr_facet_query_initial_limits', array());
1205 $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit'];
1206 variable_set('apachesolr_facet_query_initial_limits', $initial);
1207 $children = variable_get('apachesolr_facet_show_children', array());
1208 $children[$module][$delta] = (int)$edit['apachesolr_facet_show_children'];
1209 variable_set('apachesolr_facet_show_children', $children);
1210 $facet_missing = variable_get('apachesolr_facet_missing', array());
1211 $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing'];
1212 variable_set('apachesolr_facet_missing', $facet_missing);
1216 * Initialize a pager for theme('pager') without running an SQL query.
1218 * @see pager_query()
1221 * The total number of items found.
1223 * The number of items you will display per page.
1225 * An optional integer to distinguish between multiple pagers on one page.
1228 * The current page for $element. 0 by default if $_GET['page'] is empty.
1230 function apachesolr_pager_init($total, $limit = 10, $element = 0) {
1231 global $pager_page_array, $pager_total, $pager_total_items;
1232 $page = isset($_GET['page']) ?
$_GET['page'] : '';
1234 // Convert comma-separated $page to an array, used by other functions.
1235 $pager_page_array = explode(',', $page);
1237 // We calculate the total of pages as ceil(items / limit).
1238 $pager_total_items[$element] = $total;
1239 $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
1240 $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1));
1241 return $pager_page_array[$element];
1245 * This hook allows modules to modify the query and params objects.
1249 * function my_module_apachesolr_modify_query(&$query, &$params, $caller) {
1250 * // I only want to see articles by the admin!
1251 * $query->add_filter("uid", 1);
1255 function apachesolr_modify_query(&$query, &$params, $caller) {
1256 if (empty($query)) {
1257 // This should only happen if Solr is not set up - avoids fatal errors.
1261 // Call the hooks first because otherwise any modifications to the
1262 // $query object don't end up in the $params.
1263 foreach (module_implements('apachesolr_modify_query') as
$module) {
1264 $function_name = $module .
'_apachesolr_modify_query';
1265 $function_name($query, $params, $caller);
1268 // TODO: The query object should hold all the params.
1269 // Add array of fq parameters.
1270 if ($query && ($fq = $query->get_fq())) {
1271 $params['fq'] = $fq;
1273 // Add sort if present.
1275 $sort = $query->get_solrsort();
1276 $sortstring = $sort['#name'] .
' '.
$sort['#direction'];
1277 // We don't bother telling Solr to do its default sort.
1278 if ($sortstring != 'score asc') {
1279 $params['sort'] = $sortstring;
1285 * Semaphore that indicates whether a search has been done. Blocks use this
1286 * later to decide whether they should load or not.
1289 * A boolean indicating whether a search has been executed.
1292 * TRUE if a search has been executed.
1295 function apachesolr_has_searched($searched = NULL
) {
1296 static
$_searched = FALSE
;
1297 if (is_bool($searched)) {
1298 $_searched = $searched;
1304 * Factory method for solr singleton object. Structure allows for an arbitrary
1305 * number of solr objects to be used based on the host, port, path combination.
1306 * Get an instance like this:
1307 * $solr = apachesolr_get_solr();
1311 function apachesolr_get_solr($host = NULL
, $port = NULL
, $path = NULL
) {
1315 $host = variable_get('apachesolr_host', 'localhost');
1318 $port = variable_get('apachesolr_port', '8983');
1321 $path = variable_get('apachesolr_path', '/solr');
1324 if (empty($solr_cache[$host][$port][$path])) {
1325 list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service'));
1326 include_once(drupal_get_path('module', $module) .
'/'.
$filepath);
1327 $solr = new
$class($host, $port, $path);
1329 $solr_cache[$host][$port][$path] = $solr;
1331 return $solr_cache[$host][$port][$path];
1335 * Execute a search based on a query object.
1337 * Normally this function is used with the default (dismax) handler for keyword
1338 * searches. The $final_query that's returned will have been modified by
1339 * both hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().
1342 * String, name of the calling module or function for use as a cache namespace.
1343 * @param $current_query
1344 * A query object from apachesolr_drupal_query(). It will be modified by
1345 * hook_apachesolr_prepare_query() and then cached in apachesolr_current_query().
1347 * Array of parameters to pass to Solr. Must include at least 'rows'.
1349 * For paging into results, using $params['rows'] results per page.
1351 * @return array($final_query, $response)
1355 function apachesolr_do_query($caller, $current_query, &$params = array('rows' => 10), $page = 0) {
1356 // Allow modules to alter the query prior to statically caching it.
1357 // This can e.g. be used to add available sorts.
1358 foreach (module_implements('apachesolr_prepare_query') as
$module) {
1359 $function_name = $module .
'_apachesolr_prepare_query';
1360 $function_name($current_query, $params, $caller);
1363 // Cache the original query. Since all the built queries go through
1364 // this process, all the hook_invocations will happen later
1365 $query = apachesolr_current_query($current_query, $caller);
1367 // This hook allows modules to modify the query and params objects.
1368 apachesolr_modify_query($query, $params, $caller);
1369 $params['start'] = $page * $params['rows'];
1372 return array(NULL
, array());
1374 // Final chance for the caller to modify the query and params. The signature
1375 // is: CALLER_finalize_query(&$query, &$params);
1376 $function = $caller .
'_finalize_query';
1377 if (function_exists($function)) {
1378 $function($query, $params);
1381 $keys = $query->get_query_basic();
1382 if ($keys == '' && isset($params['fq'])) {
1383 // Move the fq params to q.alt for better performance.
1385 foreach ($params['fq'] as
$delta => $value) {
1386 // Move the fq param if it has no local params and is not negative.
1387 if (!preg_match('/^(?:\{!|-)/', $value)) {
1388 $qalt[] = '(' .
$value .
')';
1389 unset($params['fq'][$delta]);
1393 $params['q.alt'] = implode(' ', $qalt);
1396 // This is the object that does the communication with the solr server.
1397 $solr = apachesolr_get_solr();
1398 // We must run htmlspecialchars() here since converted entities are in the index
1399 // and thus bare entities &, > or < won't match. Single quotes are converted
1400 // too, but not double quotes since the dismax parser looks at them for
1402 $keys = htmlspecialchars($keys, ENT_NOQUOTES
, 'UTF-8');
1403 $keys = str_replace("'", ''', $keys);
1404 $response = $solr->search($keys, $params['start'], $params['rows'], $params);
1405 // The response is cached so that it is accessible to the blocks and anything
1406 // else that needs it beyond the initial search.
1407 apachesolr_static_response_cache($response, $caller);
1408 return array($query, $response);
1412 * It is important to hold on to the Solr response object for the duration of the
1413 * page request so that we can use it for things like building facet blocks.
1415 * @todo reverse the order of parameters in future branches.
1417 function apachesolr_static_response_cache($response = NULL
, $namespace = 'apachesolr_search') {
1418 static
$_response = array();
1420 if (is_object($response)) {
1421 $_response[$namespace] = clone
$response;
1423 if (!isset($_response[$namespace])) {
1424 $_response[$namespace] = NULL
;
1426 return $_response[$namespace];
1430 * Factory function for query objects.
1433 * The string that a user would type into the search box. Suitable input
1434 * may come from search_get_keys().
1437 * Key and value pairs that are applied as a filter query.
1440 * Visible string telling solr how to sort.
1443 * The search base path (without the keywords) for this query.
1445 function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '') {
1446 list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query'));
1447 module_load_include('php', $module, $class);
1450 $query = new
$class(apachesolr_get_solr(), $keys, $filters, $solrsort, $base_path);
1452 catch (Exception
$e) {
1453 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
1461 * Static getter/setter for the current query
1463 * @todo reverse the order of parameters in future branches.
1465 function apachesolr_current_query($query = NULL
, $namespace = 'apachesolr_search') {
1466 static
$saved_query = array();
1468 if (is_object($query)) {
1469 $saved_query[$namespace] = clone
$query;
1472 return is_object($saved_query[$namespace]) ? clone
$saved_query[$namespace] : NULL
;
1476 * array('index_type' => 'integer',
1477 * 'multiple' => TRUE,
1478 * 'name' => 'fieldname',
1481 function apachesolr_index_key($field) {
1482 switch ($field['index_type']) {
1493 $type_prefix = 'si';
1496 $type_prefix = 'p'; // reserve d for date
1508 $type_prefix = 'td';
1511 $type_prefix = 'ti';
1514 $type_prefix = 'tl';
1517 $type_prefix = 'tf';
1520 $type_prefix = 'tp';
1525 $sm = $field['multiple'] ?
'm_' : 's_';
1526 return $type_prefix .
$sm .
$field['name'];
1530 * Try to map a schema field name to a human-readable description.
1532 function apachesolr_field_name_map($field_name) {
1533 require_once(drupal_get_path('module', 'apachesolr') .
'/apachesolr.admin.inc');
1534 return _apachesolr_field_name_map($field_name);
1538 * Invokes hook_apachesolr_cck_fields_alter() to find out how to handle CCK fields.
1540 function apachesolr_cck_fields() {
1543 if (!isset($fields)) {
1545 // If CCK isn't enabled, do nothing.
1546 if (module_exists('content')) {
1547 module_load_include('inc', 'content', 'includes/content.crud');
1548 $cck_field_instances = content_field_instance_read();
1549 // A single default mapping for all text fields using optionwidgets.
1550 $mappings['text'] = array(
1551 'optionwidgets_select' => array('callback' => '', 'index_type' => 'string', 'facets' => TRUE
),
1552 'optionwidgets_buttons' => array('callback' => '', 'index_type' => 'string', 'facets' => TRUE
),
1553 'optionwidgets_onoff' => array('callback' => '', 'index_type' => 'string', 'facets' => TRUE
),
1555 // A single default mapping for all integer fields using optionwidgets.
1556 $mappings['number_integer'] = array(
1557 'optionwidgets_select' => array('callback' => '', 'index_type' => 'sint', 'facets' => TRUE
),
1558 'optionwidgets_buttons' => array('callback' => '', 'index_type' => 'sint', 'facets' => TRUE
),
1559 'optionwidgets_onoff' => array('callback' => '', 'index_type' => 'sint', 'facets' => TRUE
),
1561 // A field-specific mapping would look like:
1562 // $mappings['per-field']['field_model_name'] = array('callback' => '', 'index_type' => 'string', 'facets' => TRUE);
1564 // $mappings['per-field']['field_model_price'] = array('callback' => '', 'index_type' => 'float', 'facets' => TRUE);
1565 // Allow other modules to add or alter mappings.
1566 drupal_alter('apachesolr_cck_fields', $mappings);
1567 foreach ($cck_field_instances as
$instance) {
1568 $field_type = $instance['type'];
1569 $field_name = $instance['field_name'];
1570 $widget_type = $instance['widget']['type'];
1571 // Only deal with fields that have index mappings and that have not been marked for exclusion.
1572 if ((isset($mappings[$field_type][$widget_type]) ||
1573 isset($mappings['per-field'][$field_name])) &&
1574 empty($instance['display_settings'][NODE_BUILD_SEARCH_INDEX
]['exclude'])) {
1575 if (isset($mappings['per-field'][$field_name])) {
1576 $instance['index_type'] = $mappings['per-field'][$field_name]['index_type'];
1577 $instance['callback'] = $mappings['per-field'][$field_name]['callback'];
1578 // Default 'facets' to TRUE for backwards compatibility.
1579 $instance['facets'] = isset($mappings['per-field'][$field_name]['facets']) ?
$mappings['per-field'][$field_name]['facets'] : TRUE
;
1582 $instance['index_type'] = $mappings[$field_type][$widget_type]['index_type'];
1583 $instance['callback'] = $mappings[$field_type][$widget_type]['callback'];
1584 // Default 'facets' to TRUE for backwards compatibility.
1585 $instance['facets'] = isset($mappings[$field_type][$widget_type]['facets']) ?
$mappings[$field_type][$widget_type]['facets'] : TRUE
;
1587 $instance['multiple'] = (bool
) $instance['multiple'];
1588 $instance['name'] = 'cck_' .
$field_name;
1589 if (isset($fields[$field_name]) && is_array($fields[$field_name])) {
1590 // Merge together settings when used for multiple node types.
1591 $fields[$field_name] = array_merge($fields[$field_name], $instance);
1594 $fields[$field_name] = $instance;
1596 $fields[$field_name]['content_types'][] = $instance['type_name'];
1597 unset($fields[$field_name]['type_name']);
1606 * Implementation of hook_theme().
1608 function apachesolr_theme() {
1611 * Returns a link for a facet term, with the number (count) of results for that term
1613 'apachesolr_facet_link' => array(
1614 'arguments' => array('facet_text' => NULL
, 'path' => NULL
, 'options' => NULL
, 'count' => NULL
, 'active' => FALSE
, 'num_found' => NULL
),
1617 * Returns a link to remove a facet filter from the current search.
1619 'apachesolr_unclick_link' => array(
1620 'arguments' => array('facet_text' => NULL
, 'path' => NULL
, 'options' => NULL
),
1623 * Returns a list of links from the above functions (apachesolr_facet_item and apachesolr_unclick_link)
1625 'apachesolr_facet_list' => array(
1626 'arguments' => array('items' => NULL
, 'display_limit' => NULL
),
1629 * Returns a list of links generated by apachesolr_sort_link
1631 'apachesolr_sort_list' => array(
1632 'arguments' => array('items' => NULL
),
1635 * Returns a link which can be used to search the results.
1637 'apachesolr_sort_link' => array(
1638 'arguments' => array('text' => NULL
, 'path' => NULL
, 'options' => NULL
, 'active' => FALSE
, 'direction' => ''),
1641 * Returns a list of results (docs) in content recommendation block
1643 'apachesolr_mlt_recommendation_block' => array(
1644 'arguments' => array('docs' => NULL
, 'delta' => NULL
),
1650 * Performs a moreLikeThis query using the settings and retrieves documents.
1653 * An array of settings.
1655 * The Solr ID of the document for which you want related content.
1656 * For a node that is apachesolr_document_id($node->nid)
1658 * @return An array of response documents, or NULL
1660 function apachesolr_mlt_suggestions($settings, $id) {
1663 $solr = apachesolr_get_solr();
1665 'mlt_mintf' => 'mlt.mintf',
1666 'mlt_mindf' => 'mlt.mindf',
1667 'mlt_minwl' => 'mlt.minwl',
1668 'mlt_maxwl' => 'mlt.maxwl',
1669 'mlt_maxqt' => 'mlt.maxqt',
1670 'mlt_boost' => 'mlt.boost',
1671 'mlt_qf' => 'mlt.qf',
1676 'fl' => 'nid,title,path,url',
1677 'mlt.fl' => implode(',', $settings['mlt_fl']),
1680 foreach ($fields as
$form_key => $name) {
1681 if (!empty($settings[$form_key])) {
1682 $params[$name] = $settings[$form_key];
1685 $query = apachesolr_drupal_query('id:' .
$id);
1687 // This hook allows modules to modify the query and params objects.
1688 apachesolr_modify_query($query, $params, 'apachesolr_mlt');
1689 if (empty($query)) {
1693 $response = $solr->search($query->get_query_basic(), 0, $settings['num_results'], $params);
1695 if ($response->response
) {
1696 $docs = (array) end($response->response
);
1700 catch (Exception
$e) {
1701 watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL
, WATCHDOG_ERROR
);
1706 * Implementation of hook_form_[form_id]_alter().
1708 * Make sure to flush cache when content types are changed.
1710 function apachesolr_form_node_type_form_alter(&$form, $form_state) {
1711 $form['#submit'][] = 'apachesolr_clear_cache';
1715 * Implementation of hook_form_[form_id]_alter().
1717 * Make sure to flush cache when fields are added.
1719 function apachesolr_form_content_field_overview_form_alter(&$form, $form_state) {
1720 $form['#submit'][] = 'apachesolr_clear_cache';
1724 * Implementation of hook_form_[form_id]_alter().
1726 * Make sure to flush cache when fields are updated.
1728 function apachesolr_form_content_field_edit_form_alter(&$form, $form_state) {
1729 $form['#submit'][] = 'apachesolr_clear_cache';
1733 * Implementation of hook_form_[form_id]_alter
1735 function apachesolr_form_block_admin_display_form_alter(&$form) {
1736 foreach ($form as
$key => $block) {
1737 if ((strpos($key, "apachesolr_mlt-") === 0) && $block['module']['#value'] == 'apachesolr') {
1738 $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/settings/apachesolr/mlt/delete_block/'.
$block['delta']['#value']));
1744 * Implementation of hook_form_[form_id]_alter().
1746 * Hide the core 'title' field in favor of our 'name' field..
1748 function apachesolr_form_block_admin_configure_alter(&$form, $form_state) {
1749 if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') {
1750 $form['block_settings']['title']['#access'] = FALSE
;
1755 * Returns a list of blocks. Used by hook_block
1757 function apachesolr_mlt_list_blocks() {
1758 $blocks = variable_get('apachesolr_mlt_blocks', array());
1759 foreach ($blocks as
$delta => $settings) {
1760 $blocks[$delta] += array('info' => t('Apache Solr recommendations: !name', array('!name' => $settings['name'])) , 'cache' => BLOCK_CACHE_PER_PAGE
);
1765 function apachesolr_mlt_load_block($delta) {
1766 $blocks = variable_get('apachesolr_mlt_blocks', array());
1767 return isset($blocks[$delta]) ?
$blocks[$delta] : FALSE
;
1770 function theme_apachesolr_mlt_recommendation_block($docs, $delta) {
1772 foreach ($docs as
$result) {
1773 // Suitable for single-site mode.
1774 $links[] = l($result->title
, $result->path
, array('html' => TRUE
));
1776 return theme('item_list', $links);
1779 function theme_apachesolr_facet_link($facet_text, $path, $options = array(), $count, $active = FALSE
, $num_found = NULL
) {
1780 $options['attributes']['class'][] = 'apachesolr-facet';
1782 $options['attributes']['class'][] = 'active';
1784 $options['attributes']['class'] = implode(' ', $options['attributes']['class']);
1785 return apachesolr_l($facet_text .
" ($count)", $path, $options);
1789 * A replacement for l()
1790 * - doesn't add the 'active' class
1791 * - retains all $_GET parameters that ApacheSolr may not be aware of
1792 * - if set, $options['query'] MUST be an array
1794 * @see http://api.drupal.org/api/function/l/6 for parameters and options.
1797 * an HTML string containing a link to the given path.
1799 function apachesolr_l($text, $path, $options = array()) {
1800 // Merge in defaults.
1802 'attributes' => array(),
1807 // Don't need this, and just to be safe.
1808 unset($options['attributes']['title']);
1809 // Double encode + characters for clean URL Apache quirks.
1810 if (variable_get('clean_url', '0')) {
1811 $path = str_replace('+', '%2B', $path);
1814 // Retain GET parameters that ApacheSolr knows nothing about.
1815 $query = apachesolr_current_query();
1816 $get = array_diff_key($_GET, array('q' => 1, 'page' => 1), $options['query'], $query->get_url_queryvalues());
1817 $options['query'] += $get;
1819 return '<a href="'.
check_url(url($path, $options)) .
'"'.
drupal_attributes($options['attributes']) .
'>'.
($options['html'] ?
$text : check_plain(html_entity_decode($text))) .
'</a>';
1822 function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) {
1823 if (empty($options['html'])) {
1824 $facet_text = check_plain(html_entity_decode($facet_text));
1827 // Don't pass this option as TRUE into apachesolr_l().
1828 unset($options['html']);
1830 $options['attributes']['class'] = 'apachesolr-unclick';
1831 return apachesolr_l("(-)", $path, $options) .
' '.
$facet_text;
1834 function theme_apachesolr_sort_link($text, $path, $options = array(), $active = FALSE
, $direction = '') {
1837 $icon = ' '.
theme('tablesort_indicator', $direction);
1840 if (isset($options['attributes']['class'])) {
1841 $options['attributes']['class'] .
= ' active';
1844 $options['attributes']['class'] = 'active';
1847 return $icon .
apachesolr_l($text, $path, $options);
1850 function theme_apachesolr_facet_list($items, $display_limit = 0) {
1851 // theme('item_list') expects a numerically indexed array.
1852 $items = array_values($items);
1853 // If there is a limit and the facet count is over the limit, hide the rest.
1854 if (($display_limit > 0) && (count($items) > $display_limit)) {
1855 // Show/hide extra facets.
1856 drupal_add_js(drupal_get_path('module', 'apachesolr') .
'/apachesolr.js');
1857 // Split items array into displayed and hidden.
1858 $hidden_items = array_splice($items, $display_limit);
1859 foreach ($hidden_items as
$hidden_item) {
1860 if (!is_array($hidden_item)) {
1861 $hidden_item = array('data' => $hidden_item);
1863 $hidden_item['class'] = isset($hidden_item['class']) ?
$hidden_item['class'] .
' apachesolr-hidden-facet' : 'apachesolr-hidden-facet';
1864 $items[] = $hidden_item;
1868 if (user_access('administer search')) {
1869 $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters');
1871 return theme('item_list', $items) .
$admin_link;
1874 function theme_apachesolr_sort_list($items) {
1875 // theme('item_list') expects a numerically indexed array.
1876 $items = array_values($items);
1877 return theme('item_list', $items);
1881 * The interface for all 'query' objects.
1883 interface Drupal_Solr_Query_Interface
{
1885 * Get all filters, or the subset of filters for one field.
1888 * Optional name of a Solr field.
1890 function get_filters($name = NULL
);
1893 * Checks to see if a specific filter is already present.
1895 * @param string $field
1896 * the facet field to check
1898 * @param string $value
1899 * The facet value to check against
1901 function has_filter($field, $value);
1904 * Add a filter to a query.
1906 * @param string $field
1907 * the facet field to apply to this query
1909 * @param string value
1910 * the value of the facet to apply
1912 * @param boolean $exclude
1913 * Optional paramter. If TRUE, the filter will be negative,
1914 * meaning that matching values will be excluded from the
1917 function add_filter($field, $value, $exclude = FALSE
);
1920 * Remove a filter from the query.
1922 * @param string $field
1923 * the facet field to remove
1925 * @param string $value
1926 * The facet value to remove
1927 * This value can be NULL
1929 function remove_filter($field, $value = NULL
);
1932 * Get the query's keywords.
1934 function get_keys();
1937 * Set the query's keywords.
1939 * @param string $keys
1942 function set_keys($keys);
1945 * Removes the query's keywords.
1947 function remove_keys();
1950 * Return the search path (including the search keywords).
1952 function get_path();
1955 * Return filters and sort in a form suitable for a query param to url().
1957 function get_url_queryvalues();
1960 * Return the basic string query.
1962 function get_query_basic();
1965 * Return the sorts that are provided by the query object.
1967 * @return array all the sorts provided
1969 function get_available_sorts();
1972 * Make a sort available.
1974 function set_available_sort($field, $sort);
1979 * Returns the non-urlencode, non-aliased sort field and direction.
1980 * as an array keyed with '#name' and '#direction'.
1982 function get_solrsort();
1988 * The name of a field in the solr index that's an allowed sort.
1993 function set_solrsort($field, $direction);
1996 * Add a subquery to the query.
1998 * @param Drupal_Solr_Query_Interface $query
1999 * The query to add to the orginal query - may have keywords or filters.
2001 * @param string $fq_operator
2002 * The operator to use within the filter part of the subquery
2004 * @param string $q_operator
2005 * The operator to use in joining the subquery to
2006 * the main keywords. Note - this is unlikely to work
2007 * with the Dismax handler when the main query is only
2010 function add_subquery(Drupal_Solr_Query_Interface
$query, $fq_operator = 'OR', $q_operator = 'AND');
2013 * Remove a specific subquery.
2015 * @param Drupal_Solr_Query_Interface $query
2016 * the query to remove
2018 function remove_subquery(Drupal_Solr_Query_Interface
$query);
2021 * Remove all subqueries.
2023 function remove_subqueries();
2027 * Wrapper function for tt() if i18nstrings enabled.
2029 function apachesolr_tt($name, $string, $langcode = NULL
, $update = FALSE
) {
2030 if (module_exists('i18nstrings')) {
2031 return tt($name, $string, $langcode, $update);