'Apache Solr', 'description' => 'Administer Apache Solr.', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_settings'), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'file' => 'apachesolr.admin.inc', ); $items['admin/settings/apachesolr/settings'] = array( 'title' => 'Settings', 'weight' => -10, 'access arguments' => array('administer site configuration'), '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 site configuration'), '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 site configuration'), 'weight' => -8, 'file' => 'apachesolr.admin.inc', 'type' => MENU_LOCAL_TASK, ); $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, ); 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; } } /** * Implementation of hook_requirements(). */ function apachesolr_requirements($phase) { // Ensure translations don't break at install time $t = get_t(); if ($phase == 'runtime') { $host = variable_get('apachesolr_host', 'localhost'); $port = variable_get('apachesolr_port', 8983); $path = variable_get('apachesolr_path', '/solr'); $ping = FALSE; try { $solr = apachesolr_get_solr(); $ping = @$solr->ping(variable_get('apachesolr_ping_timeout', 4)); // If there is no $solr object, there is no server available, so don't continue. if (!$ping) { throw new Exception(t('No Solr instance available when checking requirements.')); } } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR); } $value = $ping ? $t('Your site has contacted the Apache Solr server.') : $t('Your site was unable to contact the Apache Solr server.'); $severity = $ping ? 0: 2; $description = theme('item_list', array($t('Host: %host', array('%host' => $host)), $t('Port: %port', array('%port' => $port)), $t('Path: %path', array('%path' => $path)))); $requirements['apachesolr'] = array( 'title' => $t('Apache Solr'), 'value' => $value, 'description' => $description, 'severity' => $severity, ); return $requirements; } } /** * 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; } function apachesolr_document_id($id, $type = 'node') { return apachesolr_site_hash() . "/$type/" . $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': 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'); } } 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 a resource from a query based on an indexing namespace. */ function apachesolr_get_nodes_to_index($namespace, $limit) { list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace); return 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); } /** * Function to handle the indexing of nodes. * * The calling function must supply a name space or track/store * the timestamp and nid returned. * Returns FALSE if no nodes were indexed (none found or error). */ function apachesolr_index_nodes($result, $namespace = '', $callback = 'apachesolr_add_node_document') { 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', $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; while ($row = db_fetch_object($result)) { // Variables to track the last item changed. $position['last_change'] = $row->changed; $position['last_nid'] = $row->nid; $callback($documents, $row->nid, $namespace); } 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_updated(time()); } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage(), NULL, 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 apachesolr_delete_node_from_index($node) { try { $solr = apachesolr_get_solr(); $solr->deleteById(apachesolr_document_id($node->nid)); apachesolr_index_updated(time()); return TRUE; } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR); return FALSE; } } /** * Helper function to keep track of when the index has been updated. */ function apachesolr_index_updated($updated = NULL) { if (isset($updated)) { if ($updated) { variable_set('apachesolr_index_updated', (int) $updated); } else { variable_del('apachesolr_index_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); } $last = variable_get('apachesolr_last_optimize', 0); $time = time(); // Make sure to omtimize once per day. if ($time - $last > 60*60*24) { $solr->optimize(FALSE, FALSE); variable_set('apachesolr_last_optimize', $time); apachesolr_index_updated($time); } // Only clear the cache if the index changed, but at least once daily. // TODO: clear on some schedule if running multi-site. $updated = apachesolr_index_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_updated(FALSE); } } } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage() . ' in apachesolr_cron', NULL, WATCHDOG_ERROR); } } /** * 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() and hook_cron(). */ 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 site configuration') && 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() and hook_cron(). */ 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 site configuration') && 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('apachesolr', $e->getMessage(), NULL, WATCHDOG_ERROR); } } /** * Return the enabled facets from the specified block array. * * @param $module * The module (optional). * @return * An array consisting 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; } /** * Implementation of hook_block(). */ function apachesolr_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list': // Add the blocks $blocks['sort'] = array( 'info' => t('Apache Solr Core: Sorting'), 'cache' => BLOCK_CACHE_PER_PAGE, ); return $blocks; case 'view': if (apachesolr_has_searched()) { // 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(); switch ($delta) { case 'sort': $sorts = $query->get_available_sorts(); $solrsorts = array(); $sort_parameter = isset($_GET['solrsort']) ? check_plain($_GET['solrsort']) : FALSE; foreach (explode(',', $sort_parameter) as $solrsort) { $parts = explode(' ', $solrsort); if (!empty($parts[0]) && !empty($parts[1])) { $solrsorts[$parts[0]] = $parts[1]; } } $sort_links = array(); $path = $query->get_path(); $new_query = clone $query; foreach ($sorts as $type => $sort) { $new_sort = isset($solrsorts[$type]) ? $solrsorts[$type] == 'asc' ? 'desc' : 'asc' : $sort['default']; $new_query->set_solrsort($type == "relevancy" ? '' : "{$type} {$new_sort}"); $active = isset($solrsorts[$type]) || ($type == "relevancy" && !$solrsorts); $direction = isset($solrsorts[$type]) ? $solrsorts[$type] : ''; $sort_links[$type] = array( 'name' => $sort['name'], 'path' => $path, 'querystring' => $new_query->get_url_querystring(), 'active' => $active, 'direction' => $direction ); } // Allow other modules to add or remove sorts. drupal_alter('apachesolr_sort_links', $sort_links); foreach ($sort_links as $type => $link) { $themed_links[$type] = theme('apachesolr_sort_link', $link['name'], $link['path'], $link['querystring'], $link['active'], $link['direction']); } return array('subject' => t('Sort by'), 'content' => theme('apachesolr_sort_list', $themed_links)); default: break; } } break; } } /** * 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)) { $contains_active = FALSE; $items = array(); foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) { $sortpre = 1000000 - $count; $options = array(); $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); } $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); $path = $new_query->get_path(); $querystring = $new_query->get_url_querystring(); $unclick_link = theme('apachesolr_unclick_link', $path, $querystring); } else { $new_query->add_filter($facet_field, $facet, $exclude); $path = $new_query->get_path(); $querystring = $new_query->get_url_querystring(); } if ($count || $active) { $items[$sortpre . '*' . $facet_text] = theme('apachesolr_facet_item', $facet_text, $count, $path, $querystring, $active, $unclick_link, $response->response->numFound, $options); } } // Unless a facet is active only display 2 or more. if ($items && ($response->response->numFound > 1 || $contains_active)) { ksort($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); 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) { // Iteratively remove the date facets. $new_query->remove_filter($facet_field, $filter['#value']); $path = $new_query->get_path(); $querystring = $new_query->get_url_querystring(); $unclick_link = theme('apachesolr_unclick_link', $path, $querystring); if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($filter['#start']); } else { $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']); } array_unshift($items, theme('apachesolr_facet_item', $facet_text, 0, $path, $querystring, TRUE, $unclick_link)); } // 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) { // 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); } 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] .']'); $path = $new_query->get_path(); $querystring = $new_query->get_url_querystring(); $items[] = theme('apachesolr_facet_item', $facet_text, $count, $path, $querystring, 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); 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 gmdate('Y', $unix); case 'MONTH': return gmdate('F Y', $unix); case 'DAY': return gmdate('F j, Y', $unix); case 'HOUR': return gmdate('g A', $unix); case 'MINUTE': return gmdate('g:i A', $unix); case 'SECOND': return gmdate('g:i:s A', $unix); } } 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', // For now, HOUR is a reasonable smallest gap. // '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()); $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), ); $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); $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); } /** * 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) { foreach (module_implements('apachesolr_modify_query') as $module) { $function_name = $module . '_apachesolr_modify_query'; $function_name($query, $params, $caller); } // Add array of fq parameters. if ($query && ($fq = $query->get_fq())) { $params['fq'] = $fq; } } /** * 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', $e->getMessage(), NULL, WATCHDOG_ERROR); return; } } return $solr_cache[$host][$port][$path]; } /** * 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. */ function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '') { list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query')); include_once drupal_get_path('module', $module) .'/'. $class .'.php'; try { $query = new $class(apachesolr_get_solr(), $keys, $filters, $solrsort, $base_path); } catch (Exception $e) { watchdog('Apache Solr', $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; default: $type_prefix = 's'; } $sm = $field['multiple'] ? 'm_' : 's_'; return $type_prefix . $sm . $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('callback' => '', 'index_type' => 'string'), 'optionwidgets_buttons' => array('callback' => '', 'index_type' => 'string') ); // A field-specific mapping would look like: // $mappings['per-field']['field_model_name'] = array('callback' => '', 'index_type' => 'string'); // or // $mappings['per-field']['field_model_price'] = array('callback' => '', 'index_type' => 'float'); // 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 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($fields[$row->field_name])) || isset($mappings['per-field'][$row->field_name])) { if (isset($mappings['per-field'][$row->field_name])) { $row->index_type = $mappings['per-field'][$row->field_name]['index_type']; $row->callback = $mappings['per-field'][$row->field_name]['callback']; } else { $row->index_type = $mappings[$row->field_type][$row->widget_type]['index_type']; $row->callback = $mappings[$row->field_type][$row->widget_type]['callback']; } $row->multiple = (bool) $row->multiple; $row->name = 'cck_' . $row->field_name; $fields[$row->field_name] = (array) $row; } } } } return $fields; } /** * Implementation of hook_theme(). */ function apachesolr_theme() { return array( 'apachesolr_facet_item' => array( 'arguments' => array('name' => NULL, 'count' => NULL, 'path' => NULL, 'querystring' => '', 'active' => FALSE, 'unclick_link' => NULL, 'num_found' => NULL, 'options' => NULL), ), 'apachesolr_unclick_link' => array( 'arguments' => array('url' => NULL, 'querystring' => ''), ), 'apachesolr_facet_list' => array( 'arguments' => array('items' => NULL), ), 'apachesolr_sort_list' => array( 'arguments' => array('items' => NULL), ), 'apachesolr_sort_link' => array( 'arguments' => array('text' => NULL, 'path' => NULL, 'querystring' => '', 'active' => FALSE, 'direction' => ''), ), ); } function theme_apachesolr_facet_item($name, $count, $path, $querystring = '', $active = FALSE, $unclick_link = NULL, $num_found = NULL, $options = array()) { if ($active) { if (isset($options['attributes']['class'])) { $options['attributes']['class'] .= ' active'; } else { $options['attributes']['class'] = 'active'; } } if ($unclick_link) { if (empty($options['html'])) { $name = check_plain($name); } return $unclick_link . ' '. $name; } else { $options['query'] = $querystring; return apachesolr_l($name ." ($count)", $path, $options); } } /** * A replacement for l() - very similar but doesn't add the 'active' class. * * @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, ); // Don't need this, and just to be safe. unset($options['attributes']['title']); return ''. ($options['html'] ? $text : check_plain($text)) .''; } function theme_apachesolr_unclick_link($path, $querystring = '') { return apachesolr_l("(-)", $path, array('query' => $querystring)); } function theme_apachesolr_sort_link($text, $path, $querystring = '', $active = FALSE, $direction = '') { $icon = ''; $attributes = array(); if ($direction) { $icon = ' '. theme('tablesort_indicator', $direction); } if ($active) { $attributes['class'] = 'active'; } return $icon . apachesolr_l($text, $path, array('attributes' => $attributes, 'query' => $querystring)); } function theme_apachesolr_facet_list($items, $display_limit = 0) { // 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)) { // Show/hide extra facets. drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js'); // Split items array into displayed and hidden. $hidden_items = array_splice($items, $display_limit); foreach ($hidden_items as $link) { $items[] = array('data' => $link, 'class' => 'apachesolr-hidden-facet'); } } $admin_link = ''; if (user_access('administer site configuration')) { $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) { 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 any query string for use in the l function. * * @see l() */ function get_url_querystring(); /** * return the basic string query */ function get_query_basic(); /** * Set the solrsort. * * @param string raw string to set the sort to */ function set_solrsort($sortstring); /** * 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); }