'Apache Solr', 'description' => 'Administer Apache Solr.', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_settings'), 'access callback' => 'user_access', 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', ); $items['admin/settings/apachesolr/settings'] = array( 'title' => 'Settings', 'weight' => -10, 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/apachesolr/enabled-filters'] = array( 'title' => 'Enabled filters', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_enabled_facets_form'), 'weight' => -7, 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/settings/apachesolr/index'] = array( 'title' => 'Search index', 'page callback' => 'apachesolr_index_page', 'access arguments' => array('administer search'), 'weight' => -8, 'file' => 'apachesolr.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/settings/apachesolr/index/confirm/clear'] = array( 'title' => 'Confirm the re-indexing of all content', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_clear_index_confirm'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/apachesolr/index/confirm/delete'] = array( 'title' => 'Confirm index deletion', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_delete_index_confirm'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/reports/apachesolr'] = array( 'title' => 'Apache Solr search index', 'page callback' => 'apachesolr_index_report', 'access arguments' => array('access site reports'), 'file' => 'apachesolr.admin.inc', ); $items['admin/reports/apachesolr/index'] = array( 'title' => 'Search index', 'file' => 'apachesolr.admin.inc', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/apachesolr/mlt/add_block'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_mlt_add_block_form'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/apachesolr/mlt/delete_block/%'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_mlt_delete_block_form', 5), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.) * Depending on the admin settings, possibly redirect to Drupal's core search. * * @param $search_name * The name of the search implementation. * * @param $querystring * The search query that was issued at the time of failure. */ function apachesolr_failure($search_name, $querystring) { $fail_rule = variable_get('apachesolr_failure', 'show_error'); switch ($fail_rule) { case 'show_error': drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error'); break; case 'show_drupal_results': drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name))); drupal_goto('search/node/' . drupal_urlencode($querystring)); break; case 'show_no_results': return; } } /** * Like $site_key in _update_refresh() - returns a site-specific hash. */ function apachesolr_site_hash() { if (!($hash = variable_get('apachesolr_site_hash', FALSE))) { global $base_url; $hash = substr(md5(md5($base_url . drupal_get_private_key() . 'apachesolr')), 0, 12); variable_set('apachesolr_site_hash', $hash); } return $hash; } /** * Generate a unique ID for an entity being indexed. * * @param $id * An id number (or string) unique to this site, such as a node ID. * @param $entity * A string like 'node', 'file', 'user', or some other Drupal object type. * * @return * A string combining the parameters with the site hash. */ function apachesolr_document_id($id, $entity = 'node') { return apachesolr_site_hash() . "/$entity/" . $id; } /** * Implementation of hook_user(). * * Mark nodes as needing re-indexing if the author name changes. */ function apachesolr_user($op, &$edit, &$account) { switch ($op) { case 'update': if (isset($edit['name']) && $account->name != $edit['name']) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid); } break; } } /** * Implementation of hook_taxonomy(). * * Mark nodes as needing re-indexing if a term name changes. */ function apachesolr_taxonomy($op, $type, $edit) { if ($type == 'term' && ($op == 'update')) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']); } // TODO: the rest, such as term deletion. } /** * Implementation of hook_comment(). * * Mark nodes as needing re-indexing if comments are added or changed. * Like search_comment(). */ function apachesolr_comment($edit, $op) { $edit = (array) $edit; switch ($op) { // Reindex the node when comments are added or changed case 'insert': case 'update': case 'delete': case 'publish': case 'unpublish': // TODO: do we want to skip this if we are excluding comments // from the index for this node type? apachesolr_mark_node($edit['nid']); break; } } /** * Mark one node as needing re-indexing. */ function apachesolr_mark_node($nid) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid); } /** * Implementation of hook_node_type(). * * Mark nodes as needing re-indexing if a node type name changes. */ function apachesolr_node_type($op, $info) { if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) { // We cannot be sure we are going before or after node module. 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); } } /** * Helper function for modules implmenting hook_search's 'status' op. */ function apachesolr_index_status($namespace) { list($excluded_types, $args, $join_sql, $exclude_sql) = apachesolr_exclude_types($namespace); $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE asn.status = 1 " . $exclude_sql, $excluded_types)); $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)); return array('remaining' => $remaining, 'total' => $total); } /** * Returns last changed and last nid for an indexing namespace. */ function apachesolr_get_last_index($namespace) { $stored = variable_get('apachesolr_index_last', array()); return isset($stored[$namespace]) ? $stored[$namespace] : array('last_change' => 0, 'last_nid' => 0); } /** * Clear a specific namespace's last changed and nid, or clear all. */ function apachesolr_clear_last_index($namespace = '') { if ($namespace) { $stored = variable_get('apachesolr_index_last', array()); unset($stored[$namespace]); variable_set('apachesolr_index_last', $stored); } else { variable_del('apachesolr_index_last'); } } /** * Truncate and rebuild the apachesolr_search_node table, reset the apachesolr_index_last variable. * This is the most complete way to force reindexing, or to build the indexing table for the * first time. * * @param $type * A single content type to be reindexed, leaving the others unaltered. */ function apachesolr_rebuild_index_table($type = NULL) { if (isset($type)) { db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type); // Populate table db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) SELECT n.nid, n.status, %d AS changed FROM {node} n WHERE n.type = '%s'", time(), $type); } else { db_query("DELETE FROM {apachesolr_search_node}"); // Populate table db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) SELECT n.nid, n.status, GREATEST(n.created, n.changed, c.last_comment_timestamp) AS changed FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid"); // Make sure no nodes end up with a timestamp that's in the future. $time = time(); db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE changed > %d", $time, $time); apachesolr_clear_last_index(); } cache_clear_all('*', 'cache_apachesolr', TRUE); } function apachesolr_exclude_types($namespace) { extract(apachesolr_get_last_index($namespace)); $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace); $args = array($last_change, $last_change, $last_nid); $join_sql = ''; $exclude_sql = ''; if ($excluded_types) { $excluded_types = array_unique($excluded_types); $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid "; $exclude_sql = "AND n.type NOT IN(". db_placeholders($excluded_types, 'varchar') .") "; $args = array_merge($args, $excluded_types); } return array($excluded_types, $args, $join_sql, $exclude_sql); } /** * Returns an array of rows from a query based on an indexing namespace. */ function apachesolr_get_nodes_to_index($namespace, $limit) { $rows = array(); if (variable_get('apachesolr_read_only', 0)) { return $rows; } list($excluded_types, $args, $join_sql, $exclude_sql) = apachesolr_exclude_types($namespace); $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); while($row = db_fetch_object($result)) { $rows[] = $row; } return $rows; } /** * Function to handle the indexing of nodes. * * The calling function must supply a namespace. * Returns FALSE if no nodes were indexed (none found or error). */ /** * Handles the indexing of nodes. * * @param array $rows * Each $row in $rows must have: * $row->nid * $row->changed * @param string $namespace * Usually the calling module. Is used as a clue for other modules * when they decide whether to create extra $documents, and is used * to track the last_index timestamp. * @return timestamp $position * Either a timestamp representing the last value of apachesolr_get_last_index * to be indexed, or FALSE if indexing failed. */ function apachesolr_index_nodes($rows, $namespace) { if (!$rows) { // Nothing to do. return FALSE; } try { // Get the $solr object $solr = apachesolr_get_solr(); // If there is no server available, don't continue. if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) { throw new Exception(t('No Solr instance available during indexing.')); } } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); return FALSE; } include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc'); $documents = array(); $old_position = apachesolr_get_last_index($namespace); $position = $old_position; // Invoke hook_apachesolr_document_handlers to find out what modules build $documents // from nodes in this namespace. $callbacks = module_invoke_all('apachesolr_document_handlers', 'node', $namespace); $callbacks = array_filter($callbacks, 'function_exists'); foreach ($rows as $row) { try { // Build node. Set reset = TRUE to avoid static caching of all nodes that get indexed. if ($node = node_load($row->nid, NULL, TRUE)) { foreach ($callbacks as $callback) { // The callback can either return a $document or an array of $documents. $documents[] = $callback($node, $namespace); } } // Variables to track the last item changed. $position['last_change'] = $row->changed; $position['last_nid'] = $row->nid; } catch (Exception $e) { // Something bad happened - don't continue. watchdog('Apache Solr', 'Error constructing documents to index:
!message', array('!message' => nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR); break; } } // Flatten $documents $tmp = array(); apachesolr_flatten_documents_array($documents, $tmp); $documents = $tmp; if (count($documents)) { try { watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents))); // Chunk the adds by 20s $docs_chunk = array_chunk($documents, 20); foreach ($docs_chunk as $docs) { $solr->addDocuments($docs); } // Set the timestamp to indicate an index update. apachesolr_index_set_last_updated(time()); } catch (Exception $e) { $nids = array(); if (!empty($docs)) { foreach ($docs as $doc) { $nids[] = $doc->nid; } } watchdog('Apache Solr', 'Indexing failed on one of the following nodes: @nids
!message', array('@nids' => implode(', ', $nids), '!message' => nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR); return FALSE; } } // Save the new position in case it changed. if ($namespace && $position != $old_position) { $stored = variable_get('apachesolr_index_last', array()); $stored[$namespace] = $position; variable_set('apachesolr_index_last', $stored); } return $position; } /** * Convert date from timestamp into ISO 8601 format. * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html */ function apachesolr_date_iso($date_timestamp) { return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp); } /** * Function to flatten documents array recursively. * * @param array $documents * The array of documents being indexed. * @param array &$tmp * A container variable that will contain the flattened array. */ function apachesolr_flatten_documents_array($documents, &$tmp) { foreach ($documents AS $index => $item) { if (is_array($item)) { apachesolr_flatten_documents_array($item, $tmp); } else { $tmp[] = $item; } } } function apachesolr_delete_node_from_index($node) { static $failed = FALSE; if ($failed) { return FALSE; } try { $solr = apachesolr_get_solr(); $solr->deleteById(apachesolr_document_id($node->nid)); apachesolr_index_set_last_updated(time()); return TRUE; } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); // Don't keep trying queries if they are failing. $failed = TRUE; return FALSE; } } /** * Set the timestamp of the last index update * @param $updated * A timestamp or zero. If zero, the variable is deleted. */ function apachesolr_index_set_last_updated($updated = 0) { if ($updated) { variable_set('apachesolr_index_updated', (int) $updated); } else { variable_del('apachesolr_index_updated'); } } /** * Get the timestamp of the last index update. * @return integer (timestamp) */ function apachesolr_index_get_last_updated() { return variable_get('apachesolr_index_updated', 0); } /** * Implementation of hook_cron(). */ function apachesolr_cron() { try { $solr = apachesolr_get_solr(); // Check for unpublished content that wasn't deleted from the index. $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"); while ($node = db_fetch_object($result)) { apachesolr_nodeapi_update($node, FALSE); } // Check for deleted content that wasn't deleted from the index. $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"); while ($node = db_fetch_object($result)) { apachesolr_nodeapi_delete($node, FALSE); } // Optimize the index (by default once a day). $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24); $last = variable_get('apachesolr_last_optimize', 0); $time = time(); if ($optimize_interval && ($time - $last > $optimize_interval)) { $solr->optimize(FALSE, FALSE); variable_set('apachesolr_last_optimize', $time); apachesolr_index_set_last_updated($time); } // Only clear the cache if the index changed. // TODO: clear on some schedule if running multi-site. $updated = apachesolr_index_get_last_updated(); if ($updated) { $solr->clearCache(); // Re-populate the luke cache. $solr->getLuke(); // TODO: an admin interface for setting this. Assume for now 5 minutes. if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) { // Clear the updated flag. apachesolr_index_set_last_updated(0); } } } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) . ' in apachesolr_cron', NULL, WATCHDOG_ERROR); } } /** * Implementation of hook_flush_caches(). */ function apachesolr_flush_caches() { return array('cache_apachesolr'); } /** * Implementation of hook_nodeapi(). */ function apachesolr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { switch ($op) { case 'delete': apachesolr_nodeapi_delete($node); break; case 'insert': // Make sure no node ends up with a timestamp that's in the future // by using time() rather than the node's changed or created timestamp. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid, $node->status, time()); break; case 'update': apachesolr_nodeapi_update($node); break; } } /** * Helper function for hook_nodeapi(). */ function apachesolr_nodeapi_delete($node, $set_message = TRUE) { if (apachesolr_delete_node_from_index($node)) { // There was no exception, so delete from the table. db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid); if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) { apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.'); } } } /** * Helper function for hook_nodeapi(). */ function apachesolr_nodeapi_update($node, $set_message = TRUE) { // Check if the node has gone from published to unpublished. if (!$node->status && db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid))) { if (apachesolr_delete_node_from_index($node)) { // There was no exception, so update the table. db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid); if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) { apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.'); } } } else { db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid); } } /** * Call drupal_set_message() with the text. * * The text is translated with t() and substituted using Solr stats. */ function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE) { try { $solr = apachesolr_get_solr(); $stats_summary = $solr->getStatsSummary(); drupal_set_message(t($text, $stats_summary), $type, FALSE); } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); } } /** * Return the enabled facets from the specified block array. * * @param $module * The module (optional). * @return * An array consisting of info for facets that have been enabled * for the specified module, or all enabled facets. */ function apachesolr_get_enabled_facets($module = NULL) { $enabled = variable_get('apachesolr_enabled_facets', array()); if (isset($module)) { return isset($enabled[$module]) ? $enabled[$module] : array(); } return $enabled; } /** * Save the enabled facets for all modules. * * @param $enabled * An array consisting of info for all enabled facets. * @return * The array consisting of info for all enabled facets. */ function apachesolr_save_enabled_facets($enabled) { variable_set('apachesolr_enabled_facets', $enabled); return $enabled; } /** * Save the enabled facets for one module. * * @param $module * The module name. * @param $facets * Associative array of $delta => $facet_field pairs. If omitted, all facets * for $module are disabled. * @return * An array consisting of info for all enabled facets. */ function apachesolr_save_module_facets($module, $facets = array()) { $enabled = variable_get('apachesolr_enabled_facets', array()); if (!empty($facets) && is_array($facets)) { $enabled[$module] = $facets; } else { unset($enabled[$module]); } variable_set('apachesolr_enabled_facets', $enabled); return $enabled; } /** * Implementation of hook_block(). */ function apachesolr_block($op = 'list', $delta = 0, $edit = array()) { static $access; switch ($op) { case 'list': // Get all of the moreLikeThis blocks that the user has created $blocks = apachesolr_mlt_list_blocks(); // Add the sort block. $blocks['sort'] = array( 'info' => t('Apache Solr Core: Sorting'), 'cache' => BLOCK_CACHE_PER_PAGE, ); return $blocks; case 'view': if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) { $suggestions = array(); // Determine whether the user can view the current node. if (!isset($access)) { $access = node_access('view', $node); } $block = apachesolr_mlt_load_block($delta); if ($access && $block) { $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid)); if (!empty($docs)) { $suggestions['subject'] = check_plain($block['name']); $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs); if (user_access('administer search')) { $suggestions['content'] .= l(t('Configure this block'),'admin/build/block/configure/apachesolr/' . $delta, array('attributes' => array('class' => 'apachesolr-mlt-admin-link'))); } } } return $suggestions; } elseif (apachesolr_has_searched() && $delta == 'sort') { // Get the query and response. Without these no blocks make sense. $response = apachesolr_static_response_cache(); if (empty($response) || ($response->response->numFound < 2)) { return; } $query = apachesolr_current_query(); $sorts = $query->get_available_sorts(); // Get the current sort as an array. $solrsort = $query->get_solrsort(); $sort_links = array(); $path = $query->get_path(); $new_query = clone $query; $toggle = array('asc' => 'desc', 'desc' => 'asc'); foreach ($sorts as $name => $sort) { $active = $solrsort['#name'] == $name; $direction = ''; $new_direction = $sort['default']; if ($name == 'score') { // We only sort by ascending score. $new_direction = 'asc'; } elseif ($active) { $direction = $solrsort['#direction']; $new_direction = $toggle[$solrsort['#direction']]; } $new_query->set_solrsort($name, $new_direction); $sort_links[$name] = array( 'title' => $sort['title'], 'path' => $path, 'options' => array('query' => $new_query->get_url_queryvalues()), 'active' => $active, 'direction' => $direction, ); } // Allow other modules to add or remove sorts. drupal_alter('apachesolr_sort_links', $sort_links); foreach ($sort_links as $name => $link) { $themed_links[$name] = theme('apachesolr_sort_link', $link['title'], $link['path'], $link['options'], $link['active'], $link['direction']); } return array('subject' => t('Sort by'), 'content' => theme('apachesolr_sort_list', $themed_links)); } break; case 'configure': if ($delta != 'sort') { require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc'); return apachesolr_mlt_block_form($delta); } break; case 'save': if ($delta != 'sort') { require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc'); apachesolr_mlt_save_block($edit, $delta); } break; } } /** * This code makes a decision whether to show a block or not. * @param $query * The current query object. * @param string $module * The module's name to whom this block belongs. * @param string $delta * The delta string the identifies the block within $module. * @return boolean * Whether the block should be visible. Other factors, like * the block system's visibility settings, apply as well. */ function apachesolr_block_visibility($query, $module, $delta) { // TYPE HIERARCHY. // If the block is configured to heed type hierarcy then it looks // to see if a suitable type filter has been chosen. If not, // the function returns. // This variable is not static cached because variable_get() already does that. $type_filters = variable_get('apachesolr_type_filter', array()); if (isset($type_filters[$module][$delta]) && $type_filters[$module][$delta] == TRUE) { $facet_info = apachesolr_get_facet_definitions(); if (isset($facet_info[$module][$delta]['content_types'])) { $has_filter = $query->get_filters($facet_info[$module][$delta]['facet_field']); $show = count($has_filter); foreach ($facet_info[$module][$delta]['content_types'] as $content_type) { if ($query->has_filter('type', $content_type)) { $show = TRUE; } } if (!$show) { return FALSE; } } } return TRUE; } function apachesolr_get_facet_definitions() { static $definitions; if (is_null($definitions)) { $operator_settings = variable_get('apachesolr_operator', array()); foreach (module_implements('apachesolr_facets') as $module) { $facets = module_invoke($module, 'apachesolr_facets'); if (!empty($facets)) { foreach ($facets as $delta => $info) { $definitions[$module][$delta] = $info; if(isset($definitions[$module][$delta])) { $definitions[$module][$delta]['operator'] = isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND'; } } } } } return $definitions; } /** * Returns a member of the facet definitions array if it contains * $field as the 'field_name' element. * * @param string $field * The field_name being sought. */ function apachesolr_get_facet_definition_by_field($field) { $definitions = apachesolr_get_facet_definitions(); foreach ($definitions as $module => $facets) { foreach ($facets as $key => $values) { if ($facet = array_search($field, $values)) { return $definitions[$module][$key]; } } } } /** * Implementation of hook_form_[form_id]_alter(). * * Hide the core 'title' field in favor of our 'name' field. * * Add a checkbox to enable type-specific visibility. */ function apachesolr_form_block_admin_configure_alter(&$form, $form_state) { // Hide the core title field. if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') { $form['block_settings']['title']['#access'] = FALSE; } // Add a type-specific visibility checkbox. $module = $form['module']['#value']; $delta = $form['delta']['#value']; $enabled_facets = apachesolr_get_enabled_facets(); // If this block isn't enabled as a facet, get out of here. if (!isset($enabled_facets[$module][$delta])) { return; } $facet_info = apachesolr_get_facet_definitions(); if (isset($facet_info[$module][$delta]['content_types'])) { $type_filter_settings = variable_get('apachesolr_type_filter', array()); // Set up some variables for the verbiage of the form element. $count = count($facet_info[$module][$delta]['content_types']); $types = format_plural($count, t('type'), t('types')); $are = format_plural($count, t('is'), t('are')); $content_types = implode(', ', $facet_info[$module][$delta]['content_types']) . '.'; $form['block_settings']['apachesolr_type_filter'] = array( '#type' => 'checkbox', '#title' => t('Show this block only when the type filter is selected for: %content_types', array('%content_types' => $content_types)), '#default_value' => isset($type_filter_settings[$module][$delta]) ? $type_filter_settings[$module][$delta] : FALSE, '#description' => t('This filter is relevant only for specific content types. Check this to display the block only when the type filter has been selected for one of the relevant content types.'), '#weight' => 11, ); } $operator_settings = variable_get('apachesolr_operator', array()); $form['block_settings']['apachesolr_operator'] = array( '#type' => 'radios', '#title' => t('Operator to use for facets'), '#options' => drupal_map_assoc(array('AND', 'OR')), '#default_value' => isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND', '#description' => t('AND filters are exclusive. OR filters are inclusive. Selecting more AND filters narrows the result set. Selecting more OR filters widens the result set.'), '#weight' => 12, ); // Add a submit handler to save the value. $form['#validate'][] = 'apachesolr_block_admin_configure_submit'; } function apachesolr_block_admin_configure_submit($form, &$form_state) { if (isset($form_state['values']['apachesolr_type_filter'])) { $type_filter_settings = variable_get('apachesolr_type_filter', array()); $module = $form_state['values']['module']; $delta = $form_state['values']['delta']; unset($type_filter_settings[$module][$delta]); $type_filter_settings[$module][$delta] = $form_state['values']['apachesolr_type_filter']; variable_set('apachesolr_type_filter', $type_filter_settings); } if (isset($form_state['values']['apachesolr_operator'])) { $operator_settings = variable_get('apachesolr_operator', array()); $module = $form_state['values']['module']; $delta = $form_state['values']['delta']; unset($operator_settings[$module][$delta]); $operator_settings[$module][$delta] = $form_state['values']['apachesolr_operator']; variable_set('apachesolr_operator', $operator_settings); } } /** * Helper function for displaying a facet block. */ function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) { if (!empty($response->facet_counts->facet_fields->$facet_field)) { $facet_query_sorts = variable_get('apachesolr_facet_query_sorts', array()); $contains_active = FALSE; $items = array(); foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) { $sortpre = 1000000 - $count; $options = array('delta' => $delta); $exclude = FALSE; // Solr sends this back if it's empty. if ($facet == '_empty_') { $exclude = TRUE; $facet = '[* TO *]'; $facet_text = theme('placeholder', t('Missing this field')); $options['html'] = TRUE; // Put this just below any active facets. // '-' sorts before all numbers, but after '*'. $sortpre = '-'; } else { $facet_text = $facet; } if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($facet, $options); } // If this block is to be alphabetically sorted, change $sortpre. if (isset($facet_query_sorts[$module][$delta]) && ($facet_query_sorts[$module][$delta] != 'count')) { $sortpre = $facet_text; } $unclick_link = ''; $active = FALSE; $new_query = clone $query; if ($query->has_filter($facet_field, $facet)) { $contains_active = $active = TRUE; // '*' sorts before all numbers. $sortpre = '*'; $new_query->remove_filter($facet_field, $facet); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options); } else { $new_query->add_filter($facet_field, $facet, $exclude); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound); } if ($count || $active) { $items[$sortpre . '*' . $facet_text] = $link; } } // Unless a facet is active only display 2 or more. if ($items && ($response->response->numFound > 1 || $contains_active)) { if (!isset($facet_query_sorts[$module][$delta]) || ($facet_query_sorts[$module][$delta] == 'index asc')) { ksort($items, SORT_STRING); } else if ($facet_query_sorts[$module][$delta] == 'index desc') { krsort($items, SORT_STRING); } // Get information needed by the rest of the blocks about limits. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array()); $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10); $output = theme('apachesolr_facet_list', $items, $limit, $delta); return array('subject' => $filter_by, 'content' => $output); } } return NULL; } /** * Helper function for displaying a date facet block. * * TODO: Refactor with apachesolr_facet_block(). */ function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) { $items = array(); $new_query = clone $query; foreach (array_reverse($new_query->get_filters($facet_field)) as $filter) { $options = array(); // Iteratively remove the date facets. $new_query->remove_filter($facet_field, $filter['#value']); if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($filter['#start'], $options); } else { $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']); } $options['query'] = $new_query->get_url_queryvalues(); array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options)); } // Add links for additional date filters. if (!empty($response->facet_counts->facet_dates->$facet_field)) { $field = clone $response->facet_counts->facet_dates->$facet_field; $end = $field->end; unset($field->end); $gap = $field->gap; unset($field->gap); // Treat each date facet as a range start, and use the next date // facet as range end. Use 'end' for the final end. $range_end = array(); foreach ($field as $facet => $count) { if (isset($prev_facet)) { $range_end[$prev_facet] = $facet; } $prev_facet = $facet; } $range_end[$prev_facet] = $end; foreach ($field as $facet => $count) { $options = array(); // Solr sends this back if it's empty. if ($facet == '_empty_' || $count == 0) { continue; } if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($facet, $options); } else { $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet); } $new_query = clone $query; $new_query->add_filter($facet_field, '['. $facet .' TO '. $range_end[$facet] .']'); $options['query'] = $new_query->get_url_queryvalues(); $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound); } } if (count($items) > 0) { // Get information needed by the rest of the blocks about limits. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array()); $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10); $output = theme('apachesolr_facet_list', $items, $limit, $delta); return array('subject' => $filter_by, 'content' => $output); } return NULL; } /** * Determine the gap in a date range query filter that we generated. * * This function assumes that the start and end dates are the * beginning and end of a single period: 1 year, month, day, hour, * minute, or second (all date range query filters we generate meet * this criteria). So, if the seconds are different, it is a second * gap. If the seconds are the same (incidentally, they will also be * 0) but the minutes are different, it is a minute gap. If the * minutes are the same but hours are different, it's an hour gap. * etc. * * @param $start * Start date as an ISO date string. * @param $end * End date as an ISO date string. * @return * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND. */ function apachesolr_date_find_query_gap($start_iso, $end_iso) { $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1); $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@'; if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) { foreach ($gaps as $gap => $idx) { if ($start[$idx] != $end[$idx]) { return $gap; } } } // can't tell return 'YEAR'; } /** * Format an ISO date string based on the gap used to generate it. * * This function assumes that gaps less than one day will be displayed * in a search context in which a larger containing gap including a * day is already displayed. So, HOUR, MINUTE, and SECOND gaps only * display time information, without date. * * @param $gap * A gap. * @param $iso * An ISO date string. * @return * A gap-appropriate formatted date. */ function apachesolr_date_format_iso_by_gap($gap, $iso) { // TODO: If we assume that multiple search queries are formatted in // order, we could store a static list of all gaps we've formatted. // Then, if we format an HOUR, MINUTE, or SECOND without previously // having formatted a DAY or later, we could include date // information. However, we'd need to do that per-field and I'm not // our callers always have field information handy. $unix = strtotime($iso); if ($unix > 0) { switch ($gap) { case 'YEAR': return format_date($unix, 'custom', 'Y', 0); case 'MONTH': return format_date($unix, 'custom', 'F Y', 0); case 'DAY': return format_date($unix, 'custom', 'F j, Y', 0); case 'HOUR': return format_date($unix, 'custom', 'g A', 0); case 'MINUTE': return format_date($unix, 'custom', 'g:i A', 0); case 'SECOND': return format_date($unix, 'custom', 'g:i:s A', 0); } } return $iso; } /** * Format the beginning of a date range query filter that we * generated. * * @param $start_iso * The start date. * @param $end_iso * The end date. * @return * A display string reprepsenting the date range, such as "January * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z" */ function apachesolr_date_format_range($start_iso, $end_iso) { $gap = apachesolr_date_find_query_gap($start_iso, $end_iso); return apachesolr_date_format_iso_by_gap($gap, $start_iso); } /** * Determine the best search gap to use for an arbitrary date range. * * Generally, we the maximum gap that fits between the start and end * date. If they are more than a year apart, 1 year; if they are more * than a month apart, 1 month; etc. * * This function uses Unix timestamps for its computation and so is * not useful for dates outside that range. * * @param $start * Start date as an ISO date string. * @param $end * End date as an ISO date string. * @return * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far * apart $start and $end are. */ function apachesolr_date_determine_gap($start, $end) { $start = strtotime($start); $end = strtotime($end); if ($end - $start >= 86400*365) { return 'YEAR'; } if (date('m', $start) != date('m', $end)) { return 'MONTH'; } if ($end - $start > 86400) { return 'DAY'; } // For now, HOUR is a reasonable smallest gap. return 'HOUR'; } /** * Return the next smaller date gap. * * @param $gap * A gap. * @return * The next smaller gap, or NULL if there is no smaller gap. */ function apachesolr_date_gap_drilldown($gap) { $drill = array( 'YEAR' => 'MONTH', 'MONTH' => 'DAY', 'DAY' => 'HOUR', 'HOUR' => 'MINUTE', ); return isset($drill[$gap]) ? $drill[$gap] : NULL; } /** * Used by the 'configure' $op of hook_block so that modules can generically set * facet limits on their blocks. */ function apachesolr_facetcount_form($module, $delta) { $initial = variable_get('apachesolr_facet_query_initial_limits', array()); $limits = variable_get('apachesolr_facet_query_limits', array()); $sorts = variable_get('apachesolr_facet_query_sorts', array()); $facet_missing = variable_get('apachesolr_facet_missing', array()); $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3)); $form['apachesolr_facet_query_initial_limit'] = array( '#type' => 'select', '#title' => t('Initial filter links'), '#options' => $limit, '#description' => t('The initial number of filter links to show in this block.'), '#default_value' => isset($initial[$module][$delta]) ? $initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10), ); $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3)); $form['apachesolr_facet_query_limit'] = array( '#type' => 'select', '#title' => t('Maximum filter links'), '#options' => $limit, '#description' => t('The maximum number of filter links to show in this block.'), '#default_value' => isset($limits[$module][$delta]) ? $limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20), ); // TODO: Generalize how we know what type a facet block is by putting field // type into the facet definition. 'created' and 'changed' are date blocks. if ($delta != 'created' && $delta != 'changed') { $form['apachesolr_facet_query_sort'] = array( '#type' => 'radios', '#title' => t('Sort order of facet links'), '#options' => array('count' => t('Count'), 'index asc' => t('Alphanumeric, ascending'), 'index desc' => t('Alphanumeric, descending')), '#description' => t('The sort order of facet links in this block. %Count, which is the default, will show facets with the most results first. %Alphanumeric will sort alphabetically, either ascending or descending.', array('%Count' => t('Count'), '%Alphanumeric' => t('Alphanumeric'))), '#default_value' => isset($sorts[$module][$delta]) ? $sorts[$module][$delta] : 'count', ); } $form['apachesolr_facet_missing'] = array( '#type' => 'radios', '#title' => t('Include a facet for missing'), '#options' => array(0 => t('No'), 1 => t('Yes')), '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'), '#default_value' => isset($facet_missing[$module][$delta]) ? $facet_missing[$module][$delta] : 0, ); return $form; } /** * Used by the 'save' $op of hook_block so that modules can generically set * facet limits on their blocks. */ function apachesolr_facetcount_save($edit) { // Save query limits $module = $edit['module']; $delta = $edit['delta']; $limits = variable_get('apachesolr_facet_query_limits', array()); $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit']; variable_set('apachesolr_facet_query_limits', $limits); $sorts = variable_get('apachesolr_facet_query_sorts', array()); $sorts[$module][$delta] = $edit['apachesolr_facet_query_sort']; variable_set('apachesolr_facet_query_sorts', $sorts); $initial = variable_get('apachesolr_facet_query_initial_limits', array()); $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit']; variable_set('apachesolr_facet_query_initial_limits', $initial); $facet_missing = variable_get('apachesolr_facet_missing', array()); $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing']; variable_set('apachesolr_facet_missing', $facet_missing); } /** * Initialize a pager for theme('pager') without running an SQL query. * * @see pager_query() * * @param $total * The total number of items found. * @param $limit * The number of items you will display per page. * @param $element * An optional integer to distinguish between multiple pagers on one page. * * @return * The current page for $element. 0 by default if $_GET['page'] is empty. */ function apachesolr_pager_init($total, $limit = 10, $element = 0) { global $pager_page_array, $pager_total, $pager_total_items; $page = isset($_GET['page']) ? $_GET['page'] : ''; // Convert comma-separated $page to an array, used by other functions. $pager_page_array = explode(',', $page); // We calculate the total of pages as ceil(items / limit). $pager_total_items[$element] = $total; $pager_total[$element] = ceil($pager_total_items[$element] / $limit); $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1)); return $pager_page_array[$element]; } /** * This hook allows modules to modify the query and params objects. * * Example: * * function my_module_apachesolr_modify_query(&$query, &$params) { * // I only want to see articles by the admin! * $query->add_filter("uid", 1); * * } */ function apachesolr_modify_query(&$query, &$params, $caller) { if (empty($query)) { // This should only happen if Solr is not set up - avoids fatal errors. return; } // Call the hooks first because otherwise any modifications to the // $query object don't end up in the $params. foreach (module_implements('apachesolr_modify_query') as $module) { $function_name = $module . '_apachesolr_modify_query'; $function_name($query, $params, $caller); } // TODO: The query object should hold all the params. // Add array of fq parameters. if ($query && ($fq = $query->get_fq())) { foreach ($fq as $delta => $values) { if (is_array($values) || is_object($values)) { foreach ($values as $value) { $params['fq'][$delta][] = $value; } } } } // Add sort if present. if ($query) { $sort = $query->get_solrsort(); $sortstring = $sort['#name'] .' '. $sort['#direction']; // We don't bother telling Solr to do its default sort. if ($sortstring != 'score asc') { $params['sort'] = $sortstring; } } $ors = array(); $facet_info = apachesolr_get_facet_definitions(); foreach ($facet_info as $infos) { foreach($infos as $delta => $facet) { if ($facet['operator'] == 'OR') { $ors[] = $delta; } } } if ($filter_queries = $params['fq']) { foreach ($filter_queries as $delta => $value) { $fq = $tag = ''; $op = 'AND'; // CCK facet field block deltas are not the same as their Solr index field names. $cck_delta = ''; if (strpos($delta, '_cck_')) { $cck_delta = trim(drupal_substr($delta, 7, drupal_strlen($delta))); } if (in_array($delta, $ors) || in_array($cck_delta, $ors)) { $tag = "{!tag=$delta}"; $op = 'OR'; } $fq = implode(" $op ", $params['fq'][$delta]); $params['fq'][] = $tag . $fq; unset($params['fq'][$delta]); } } } /** * Semaphore that indicates whether a search has been done. Blocks use this * later to decide whether they should load or not. * * @param $searched * A boolean indicating whether a search has been executed. * * @return * TRUE if a search has been executed. * FALSE otherwise. */ function apachesolr_has_searched($searched = NULL) { static $_searched = FALSE; if (is_bool($searched)) { $_searched = $searched; } return $_searched; } /** * Factory method for solr singleton object. Structure allows for an arbitrary * number of solr objects to be used based on the host, port, path combination. * Get an instance like this: * $solr = apachesolr_get_solr(); */ function apachesolr_get_solr($host = NULL, $port = NULL, $path = NULL) { static $solr_cache; if (empty($host)) { $host = variable_get('apachesolr_host', 'localhost'); } if (empty($port)) { $port = variable_get('apachesolr_port', '8983'); } if (empty($path)) { $path = variable_get('apachesolr_path', '/solr'); } if (empty($solr_cache[$host][$port][$path])) { list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service')); include_once(drupal_get_path('module', $module) .'/'. $filepath); try { $solr_cache[$host][$port][$path] = new $class($host, $port, $path); } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); return; } } return $solr_cache[$host][$port][$path]; } /** * Checks if a specific Apache Solr server is available. * * @return boolean TRUE if the server can be pinged, FALSE otherwise. */ function apachesolr_server_status($host = NULL, $port = NULL, $path = NULL) { if (empty($host)) { $host = variable_get('apachesolr_host', 'localhost'); } if (empty($port)) { $port = variable_get('apachesolr_port', '8983'); } if (empty($path)) { $path = variable_get('apachesolr_path', '/solr'); } $ping = FALSE; try { $solr = apachesolr_get_solr($host, $port, $path); $ping = @$solr->ping(variable_get('apachesolr_ping_timeout', 4)); } catch (Exception $e) { watchdog('Apache Solr', check_plain($e->getMessage()), NULL, WATCHDOG_ERROR); } return $ping; } /** * It is important to hold on to the Solr response object for the duration of the * page request so that we can use it for things like building facet blocks. */ function apachesolr_static_response_cache($response = NULL) { static $_response; if (!empty($response)) { $_response = clone $response; } return $_response; } /** * Factory function for query objects. * * @param $keys * The string that a user would type into the search box. Suitable input * may come from search_get_keys(). * * @param $filters * Key and value pairs that are applied as a filter query. * * @param $solrsort * Visible string telling solr how to sort. * * @param $base_path * The search base path (without the keywords) for this query. * * @param $solr * An instance of Drupal_Apache_Solr_Service. */ function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '', $solr = NULL) { list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query')); include_once drupal_get_path('module', $module) .'/'. $class .'.php'; if (empty($solr)) { $solr = apachesolr_get_solr(); } try { $query = new $class($solr, $keys, $filters, $solrsort, $base_path); } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); $query = NULL; } return $query; } /** * Static getter/setter for the current query */ function apachesolr_current_query($query = NULL) { static $saved_query = NULL; if (is_object($query)) { $saved_query = clone $query; } return empty($saved_query) ? $saved_query : clone $saved_query; } /** * array('index_type' => 'integer', * 'multiple' => TRUE, * 'name' => 'fieldname', * ), */ function apachesolr_index_key($field) { switch ($field['index_type']) { case 'text': $type_prefix = 't'; break; case 'string': $type_prefix = 's'; break; case 'integer': $type_prefix = 'i'; break; case 'sint': $type_prefix = 'si'; break; case 'double': $type_prefix = 'p'; // reserve d for date break; case 'boolean': $type_prefix = 'b'; break; case 'date': $type_prefix = 'd'; break; case 'float': $type_prefix = 'f'; break; case 'tdate': $type_prefix = 'td'; break; case 'tint': $type_prefix = 'ti'; break; case 'tlong'; $type_prefix = 'tl'; break; case 'tfloat': $type_prefix = 'tf'; break; case 'tdouble': $type_prefix = 'tp'; break; default: $type_prefix = 's'; } $sm = $field['multiple'] ? 'm_' : 's_'; return $type_prefix . $sm . $field['name']; } /** * Try to map a schema field name to a human-readable description. */ function apachesolr_field_name_map($field_name) { static $map; if (!isset($map)) { $map = array( 'body' => t('Body text - the full, rendered content'), 'title' => t('Title'), 'teaser' => t('Teaser'), 'name' => t('Author name'), 'path_alias' => t('Path alias'), 'taxonomy_names' => t('All taxonomy term names'), 'tags_h1' => t('Body text inside H1 tags'), 'tags_h2_h3' => t('Body text inside H2 or H3 tags'), 'tags_h4_h5_h6' => t('Body text inside H4, H4, or H6 tags'), 'tags_inline' => t('Body text in inline tags like EM or STRONG'), 'tags_a' => t('Body text inside links (A tags)'), 'tid' => t('Taxonomy term IDs'), ); if (module_exists('taxonomy')) { foreach(taxonomy_get_vocabularies() as $vocab) { $map['ts_vid_'. $vocab->vid .'_names'] = t('Taxonomy term names only from the %name vocabulary', array('%name' => $vocab->name)); $map['im_vid_'. $vocab->vid] = t('Taxonomy term IDs from the %name vocabulary', array('%name' => $vocab->name)); } } foreach (apachesolr_cck_fields() as $name => $field) { $map[apachesolr_index_key($field)] = t('CCK @type field %label', array('@type' => $field['index_type'], '%label' => $field['label'])); } drupal_alter('apachesolr_field_name_map', $map); } return isset($map[$field_name]) ? $map[$field_name] : $field_name; } /** * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields. */ function apachesolr_cck_fields() { static $fields; if (is_null($fields)) { $fields = array(); // If CCK isn't enabled, do nothing. if (module_exists('content')) { // A single default mapping for all text fields. $mappings['text'] = array( 'optionwidgets_select' => array( 'display_callback' => 'apachesolr_cck_text_field_callback', 'indexing_callback' => 'apachesolr_cck_text_indexing_callback', 'index_type' => 'string', ), 'optionwidgets_buttons' => array( 'display_callback' => 'apachesolr_cck_text_field_callback', 'indexing_callback' => 'apachesolr_cck_text_indexing_callback', 'index_type' => 'string', ), ); $mappings['nodereference'] = array( 'nodereference_buttons' => array( 'display_callback' => 'apachesolr_cck_nodereference_field_callback', 'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback', 'index_type' => 'integer', ), 'nodereference_select' => array( 'display_callback' => 'apachesolr_cck_nodereference_field_callback', 'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback', 'index_type' => 'integer', ), 'nodereference_autocomplete' => array( 'display_callback' => 'apachesolr_cck_nodereference_field_callback', 'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback', 'index_type' => 'integer', ), ); $mappings['userreference'] = array( 'userreference_buttons' => array( 'display_callback' => 'apachesolr_cck_userreference_field_callback', 'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback', 'index_type' => 'integer', ), 'userreference_select' => array( 'display_callback' => 'apachesolr_cck_userreference_field_callback', 'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback', 'index_type' => 'integer', ), 'userreference_autocomplete' => array( 'display_callback' => 'apachesolr_cck_userreference_field_callback', 'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback', 'index_type' => 'integer', ), ); // Allow other modules to add or alter mappings. drupal_alter('apachesolr_cck_fields', $mappings); $result = db_query("SELECT i.field_name, f.multiple, f.type AS field_type, i.widget_type, i.label, i.type_name AS content_type FROM {content_node_field_instance} i INNER JOIN {content_node_field} f ON i.field_name = f.field_name;"); while ($row = db_fetch_object($result)) { // Only deal with fields that have option widgets (facets don't make sense otherwise), or fields that have specific mappings. if ((isset($mappings[$row->field_type][$row->widget_type]) || isset($mappings['per-field'][$row->field_name]))) { if (isset($mappings['per-field'][$row->field_name])) { $field = $mappings['per-field'][$row->field_name]; $fields[$row->field_name] = $field; } else { $field = $mappings[$row->field_type][$row->widget_type]; } // It's important that we put the 'cck_' here because several points in the later processing // depend on it to route program flow to cck specific handlers. $row->name = 'cck_' . $row->field_name; $row->index_type = $field['index_type']; $row->indexing_callback = $field['indexing_callback']; $row->display_callback = $field['display_callback']; $row->facet_block_callback = $field['facet_block_callback']; $row->multiple = (bool) $row->multiple; $fields[$row->field_name] = array_merge((array) $fields[$row->field_name], (array) $row); $fields[$row->field_name]['content_types'][] = $row->content_type; unset($fields[$row->field_name]['content_type']); } } } } return $fields; } /** * Implementation of hook_content_fieldapi */ function apachesolr_content_fieldapi($op) { if ($op == 'update instance') { cache_clear_all('*', 'cache_apachesolr', TRUE); } } /** * Use the content.module's content_format() to format the * field based on its value ($facet). * * @param $facet string * The indexed value * @param $options * An array of options including the hook_block $delta. */ function apachesolr_cck_text_field_callback($facet, $options) { if (function_exists('content_format')) { return content_format($options['delta'], array('value' => $facet)); } else { return $facet; } } /** * Use the content.module's content_format() to format the * field based on its nid ($facet). * * @param $facet string * The indexed value * @param $options * An array of options including the hook_block $delta. */ function apachesolr_cck_nodereference_field_callback($facet, $options) { if (function_exists('content_format')) { return strip_tags(content_format($options['delta'], array('nid' => $facet))); } else { return $facet; } } /** * Use the content.module's content_format() to format the * field based on its uid ($facet). * * @param $facet string * The indexed value * @param $options * An array of options including the hook_block $delta. */ function apachesolr_cck_userreference_field_callback($facet, $options) { if (function_exists('content_format')) { return strip_tags(content_format($options['delta'], array('uid' => $facet))); } else { return $facet; } } /** * Implementation of hook_theme(). */ function apachesolr_theme() { return array( /** * Returns a link for a facet term, with the number (count) of results for that term */ 'apachesolr_facet_link' => array( 'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL, 'count' => NULL, 'active' => FALSE, 'num_found' => NULL), ), /** * Returns a link to remove a facet filter from the current search. */ 'apachesolr_unclick_link' => array( 'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL), ), /** * Returns a list of links from the above functions (apachesolr_facet_item and apachesolr_unclick_link) */ 'apachesolr_facet_list' => array( 'arguments' => array('items' => NULL, 'display_limit' => 0, 'delta' => ''), ), /** * Returns a list of links generated by apachesolr_sort_link */ 'apachesolr_sort_list' => array( 'arguments' => array('items' => NULL), ), /** * Returns a link which can be used to search the results. */ 'apachesolr_sort_link' => array( 'arguments' => array('text' => NULL, 'path' => NULL, 'options' => NULL, 'active' => FALSE, 'direction' => ''), ), /** * Returns a list of results (docs) in content recommendation block */ 'apachesolr_mlt_recommendation_block' => array( 'arguments' => array('docs' => NULL), ), ); } /** * Performs a moreLikeThis query using the settings and retrieves documents. * * @param $settings * An array of settings. * @param $id * The Solr ID of the document for which you want related content. * For a node that is apachesolr_document_id($node->nid) * * @return An array of response documents, or NULL */ function apachesolr_mlt_suggestions($settings, $id) { try { $solr = apachesolr_get_solr(); $fields = array( 'mlt_mintf' => 'mlt.mintf', 'mlt_mindf' => 'mlt.mindf', 'mlt_minwl' => 'mlt.minwl', 'mlt_maxwl' => 'mlt.maxwl', 'mlt_maxqt' => 'mlt.maxqt', 'mlt_boost' => 'mlt.boost', 'mlt_qf' => 'mlt.qf', ); $params = array( 'qt' => 'mlt', 'fl' => 'nid,title,path,url', 'mlt.fl' => implode(',', $settings['mlt_fl']), ); foreach ($fields as $form_key => $name) { if (!empty($settings[$form_key])) { $params[$name] = $settings[$form_key]; } } $query = apachesolr_drupal_query('id:' . $id); $type_filters = array(); if (is_array($settings['mlt_type_filters'])) { foreach ($settings['mlt_type_filters'] as $type_filter) { $type_filters[] = "type:{$type_filter}"; } $params['fq']['mlt'][] = '(' . implode(' OR ', $type_filters) . ') '; } if ($custom_filters = $settings['mlt_custom_filters']) { $params['fq']['mlt'][] = $custom_filters; } // This hook allows modules to modify the query and params objects. apachesolr_modify_query($query, $params, 'apachesolr_mlt'); if (empty($query)) { return; } $response = $solr->search($query->get_query_basic(), 0, $settings['num_results'], $params); if ($response->response) { $docs = (array) end($response->response); return $docs; } } catch ( Exception $e ) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR ); } } /** * Implementation of hook_form_[form_id]_alter */ function apachesolr_form_block_admin_display_form_alter(&$form) { foreach ($form as $key => $block) { if ((strpos($key, "apachesolr_mlt-") === 0) && $block['module']['#value'] == 'apachesolr') { $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/settings/apachesolr/mlt/delete_block/'. $block['delta']['#value'])); } } } /** * Returns a list of blocks. Used by hook_block */ function apachesolr_mlt_list_blocks() { $blocks = variable_get('apachesolr_mlt_blocks', array()); foreach ($blocks as $delta => $settings) { $blocks[$delta] += array('info' => t('Apache Solr recommendations: !name', array('!name' => $settings['name'])) , 'cache' => BLOCK_CACHE_PER_PAGE); } return $blocks; } function apachesolr_mlt_load_block($delta) { $blocks = variable_get('apachesolr_mlt_blocks', array()); return isset($blocks[$delta]) ? $blocks[$delta] : FALSE; } function theme_apachesolr_mlt_recommendation_block($docs) { $links = array(); foreach ($docs as $result) { // Suitable for single-site mode. $links[] = l($result->title, $result->path, array('html' => TRUE)); } return theme('item_list', $links); } function theme_apachesolr_facet_link($facet_text, $path, $options = array(), $count, $active = FALSE, $num_found = NULL) { $options['attributes']['class'][] = 'apachesolr-facet'; if ($active) { $options['attributes']['class'][] = 'active'; } $options['attributes']['class'] = implode(' ', $options['attributes']['class']); return apachesolr_l($facet_text ." ($count)", $path, $options); } /** * A replacement for l() * - doesn't add the 'active' class * - retains all $_GET parameters that ApacheSolr may not be aware of * - if set, $options['query'] MUST be an array * * @see http://api.drupal.org/api/function/l/6 for parameters and options. * * @return * an HTML string containing a link to the given path. */ function apachesolr_l($text, $path, $options = array()) { // Merge in defaults. $options += array( 'attributes' => array(), 'html' => FALSE, 'query' => array(), ); // Don't need this, and just to be safe. unset($options['attributes']['title']); // Double encode + characters for clean URL Apache quirks. if (variable_get('clean_url', '0')) { $path = str_replace('+', '%2B', $path); } // Retain GET parameters that ApacheSolr knows nothing about. $query = apachesolr_current_query(); $get = array_diff_key($_GET, array('q' => 1, 'page' => 1), $options['query'], $query->get_url_queryvalues()); $options['query'] += $get; return ''. ($options['html'] ? $text : check_plain($text)) .''; } function apachesolr_js() { static $settings; // Only add the js stuff once. if (empty($settings)) { $settings['apachesolr_facetstyle'] = variable_get('apachesolr_facetstyle', 'checkboxes'); // This code looks for enabled facet blocks and injects the block #ids into // Drupal.settings as jQuery selectors to add the Show more links. $show_more_blocks = array(); $facet_map = array(); foreach (apachesolr_get_facet_definitions() as $module => $definitions) { foreach ($definitions as $facet => $facet_definition) { $facet_map[$facet_definition['facet_field']] = $facet; } } foreach (apachesolr_get_enabled_facets() as $module => $blocks) { foreach ($blocks as $block) { $show_more_selector[] = "#block-{$module}-{$facet_map[$block]}:has(.apachesolr-hidden-facet)"; } } $settings['apachesolr_show_more_blocks'] = implode(', ', $show_more_selector); drupal_add_js($settings, 'setting'); drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js'); } } function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) { apachesolr_js(); if (empty($options['html'])) { $facet_text = check_plain($facet_text); } else { // Don't pass this option as TRUE into apachesolr_l(). unset($options['html']); } $options['attributes']['class'] = 'apachesolr-unclick'; return apachesolr_l("(-)", $path, $options) . ' '. $facet_text; } function theme_apachesolr_sort_link($text, $path, $options = array(), $active = FALSE, $direction = '') { $icon = ''; if ($direction) { $icon = ' '. theme('tablesort_indicator', $direction); } if ($active) { if (isset($options['attributes']['class'])) { $options['attributes']['class'] .= ' active'; } else { $options['attributes']['class'] = 'active'; } } return $icon . apachesolr_l($text, $path, $options); } function theme_apachesolr_facet_list($items, $display_limit = 0, $delta = '') { apachesolr_js(); // theme('item_list') expects a numerically indexed array. $items = array_values($items); // If there is a limit and the facet count is over the limit, hide the rest. if (($display_limit > 0) && (count($items) > $display_limit)) { // Split items array into displayed and hidden. $hidden_items = array_splice($items, $display_limit); foreach ($hidden_items as $hidden_item) { if (!is_array($hidden_item)) { $hidden_item = array('data' => $hidden_item); } $hidden_item['class'] = isset($hidden_item['class']) ? $hidden_item['class'] . ' apachesolr-hidden-facet' : 'apachesolr-hidden-facet'; $items[] = $hidden_item; } } $admin_link = ''; if (user_access('administer search')) { $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters'); } return theme('item_list', $items) . $admin_link; } function theme_apachesolr_sort_list($items) { // theme('item_list') expects a numerically indexed array. $items = array_values($items); return theme('item_list', $items); } /** * The interface for all 'query' objects. */ interface Drupal_Solr_Query_Interface { /** * Checks to see if a specific filter is already present. * * @param string $field * the facet field to check * * @param string $value * The facet value to check against */ function has_filter($field, $value); /** * Remove a filter from the query * * @param string $field * the facet field to remove * * @param string $value * The facet value to remove * This value can be NULL */ function remove_filter($field, $value = NULL); /** * Add a filter to a query * * @param string $field * the facet field to apply to this query * * @param string value * the value of the facet to apply * * @param boolean $exclude * Optional paramter. If TRUE, the filter will be negative, * meaning that matching values will be excluded from the * result set. */ function add_filter($field, $value, $exclude = FALSE); /** * Return the search path (including the search keywords). */ function get_path(); /** * Return the query's keywords. */ function get_keys(); /** * Set the query's keywords. * * @param string keys * The new keywords. */ function set_keys($keys); /** * Return an array of parameters for use in the l function. * * @see l() */ function get_url_queryvalues(); /** * return the basic string query */ function get_query_basic(); /** * Set the solrsort. * * @param $field * The name of a field in the solr index that's an allowed sort. * * @param $direction * 'asc' or 'desc' */ function set_solrsort($field, $direction); /** * Get the solrsort. * * Returns the non-urlencode, non-aliased sort field and direction. * as an array keyed with '#name' and '#direction'. */ function get_solrsort(); /** * Return an array of all filters. */ function get_filters($name = NULL); /** * Add a subquery to the query. * * @param Drupal_Solr_Query_Interface $query * The query to add to the orginal query - may have keywords or filters. * * @param string $fq_operator * The operator to use within the filter part of the subquery * * @param string $q_operator * The operator to use in joining the subquery to * the main keywords. Note - this is unlikely to work * with the Dismax handler when the main query is only * keywords. */ function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND'); /** * return the sorts that are provided by the query object * * @return array all the sorts provided */ function get_available_sorts(); /** * Remove a specific subquery * * @param Drupal_Solr_Query_Interface $query * the query to remove */ function remove_subquery(Drupal_Solr_Query_Interface $query); /** * remove all subqueries */ function remove_subqueries(); /** * make a sort available */ function set_available_sort($field, $sort); }