apachesolr_search.module in Apache Solr Search 6
Same filename and directory in other branches
Provides a content search implementation for node content for use with the Apache Solr search application.
File
apachesolr_search.moduleView source
<?php
/**
* @file
* Provides a content search implementation for node content for use with the
* Apache Solr search application.
*/
/**
* Implementation of hook_help().
*/
function apachesolr_search_help($section) {
switch ($section) {
case 'admin/settings/apachesolr/index':
if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE) == APACHESOLR_READ_ONLY) {
return t('Operating in read-only mode; updates are disabled.');
}
$remaining = 0;
$total = 0;
// Collect the stats
$status = apachesolr_index_status('apachesolr_search');
$remaining += $status['remaining'];
$total += $status['total'];
return t('The search index is generated by !cron. %percentage of the site content has been sent to the server. There @items left to send.', array(
'!cron' => l(t('running cron'), 'admin/reports/status/run-cron', array(
'query' => array(
'destination' => 'admin/settings/apachesolr/index',
),
)),
'%percentage' => (int) min(100, 100 * ($total - $remaining) / max(1, $total)) . '%',
'@items' => format_plural($remaining, t('is 1 item'), t('are @count items')),
));
}
}
/**
* Implementation of hook_menu().
*/
function apachesolr_search_menu() {
$items['admin/settings/apachesolr/query-fields'] = array(
'title' => 'Search fields',
'page callback' => 'apachesolr_search_settings_page',
'access arguments' => array(
'administer search',
),
'weight' => 1,
'type' => MENU_LOCAL_TASK,
'file' => 'apachesolr_search.admin.inc',
);
$items['admin/settings/apachesolr/content-bias'] = array(
'title' => 'Content bias settings',
'page callback' => 'apachesolr_boost_settings_page',
'access arguments' => array(
'administer search',
),
'weight' => 1,
'type' => MENU_LOCAL_TASK,
'file' => 'apachesolr_search.admin.inc',
);
return $items;
}
/**
* Implementation of hook_menu_alter().
*/
function apachesolr_search_menu_alter(&$menu) {
if (isset($menu['search/apachesolr_search/%menu_tail'])) {
$menu['search']['page callback'] = 'apachesolr_search_view';
$menu['search/apachesolr_search/%menu_tail']['page callback'] = 'apachesolr_search_view';
}
if (variable_get('apachesolr_search_make_default', 0)) {
if (isset($menu['search/node/%menu_tail'])) {
// Hide the node search tab.
$menu['search/node/%menu_tail']['type'] = MENU_CALLBACK;
unset($menu['search/node/%menu_tail']['title callback'], $menu['search/node/%menu_tail']['title arguments']);
$menu['search/node/%menu_tail']['title'] = 'Search';
}
if (isset($menu['search/apachesolr_search/%menu_tail'])) {
// Alter the solr search tab
$menu['search/apachesolr_search/%menu_tail']['weight'] = -10;
unset($menu['search/apachesolr_search/%menu_tail']['title callback'], $menu['search/apachesolr_search/%menu_tail']['title arguments']);
$menu['search/apachesolr_search/%menu_tail']['title'] = 'Content';
}
}
if (variable_get('apachesolr_search_taxonomy_links', 0)) {
if (isset($menu['taxonomy/term/%'])) {
$menu['taxonomy/term/%']['module'] = 'apachesolr_search';
$menu['taxonomy/term/%']['page callback'] = 'apachesolr_search_taxonomy_term_page';
$menu['taxonomy/term/%']['file path'] = drupal_get_path('module', 'apachesolr_search');
$menu['taxonomy/term/%']['file'] = 'apachesolr.taxonomy.inc';
}
}
}
/**
* Implementation of hook_update_index().
*/
function apachesolr_search_update_index() {
$cron_limit = variable_get('apachesolr_cron_limit', 50);
$rows = apachesolr_get_nodes_to_index('apachesolr_search', $cron_limit);
apachesolr_index_nodes($rows, 'apachesolr_search');
}
/**
* Implementation of hook_apachesolr_types_exclude().
*/
function apachesolr_search_apachesolr_types_exclude($namespace) {
if ($namespace == 'apachesolr_search') {
$excluded_types = variable_get('apachesolr_search_excluded_types', array());
return array_filter($excluded_types);
}
}
/**
* Implementation of hook_search().
*/
function apachesolr_search_search($op = 'search', $keys = NULL) {
switch ($op) {
case 'name':
return t('Search');
case 'reset':
apachesolr_clear_last_index('apachesolr_search');
return;
case 'status':
return apachesolr_index_status('apachesolr_search');
case 'search':
$filters = isset($_GET['filters']) ? $_GET['filters'] : '';
$solrsort = isset($_GET['solrsort']) ? $_GET['solrsort'] : '';
$page = isset($_GET['page']) ? $_GET['page'] : 0;
if ($keys) {
// Don't allow local params to pass through to EDismax from the url.
// We also remove any remaining leading {! since that causes a parse
// error in Solr.
$keys = preg_replace('/^(?:{![^}]*}\\s*)*(?:{!\\s*)*/', ' ', $keys);
}
try {
$results = apachesolr_search_execute($keys, $filters, $solrsort, 'search/' . arg(1), $page);
return $results;
} catch (Exception $e) {
watchdog('Apache Solr', nl2br(check_plain($e
->getMessage())), NULL, WATCHDOG_ERROR);
apachesolr_failure(t('Solr search'), $keys);
}
break;
}
// switch
}
/**
* Re-implementation of search_view().
*/
function apachesolr_search_view($type = NULL) {
if (empty($type)) {
// Note: search/X can not be a default tab because it would take on the
// path of its parent (search). It would prevent remembering keywords when
// switching tabs. This is why we drupal_goto to it from the parent instead.
drupal_goto('search/apachesolr_search');
}
$keys = trim(search_get_keys());
// Construct the search form. If the user submits POST data, this will
// redirect to a GET request before the search actually runs.
if (isset($_POST['form_id']) && $_POST['form_id'] == 'search_form') {
$form = drupal_get_form('search_form', NULL, $keys, $type);
}
// We did not redirect, so run the search if needed.
$results = '';
$filters = '';
if (isset($_GET['filters'])) {
$filters = trim($_GET['filters']);
}
// Only perform search if there is non-whitespace search term or filters:
if ($keys || $filters) {
// Log the search keys:
$log = $keys;
if ($filters) {
$log .= 'filters=' . $filters;
}
watchdog('search', '%keys (@type).', array(
'%keys' => $log,
'@type' => t('Search'),
), WATCHDOG_NOTICE, l(t('results'), 'search/' . $type . '/' . $keys));
// Collect the search results:
$results = search_data($keys, $type);
if ($results) {
$results = theme('box', t('Search results'), $results);
}
else {
$results = theme('box', t('Your search yielded no results'), variable_get('apachesolr_search_noresults', apachesolr_search_noresults()));
}
}
if (empty($form)) {
// The form may be altered based on the query that was run.
$form = drupal_get_form('search_form', NULL, $keys, $type);
}
return theme('apachesolr_search_results_page', $form, $results);
}
function apachesolr_search_noresults() {
return t('<ul>
<li>Check if your spelling is correct, or try removing filters.</li>
<li>Remove quotes around phrases to match each word individually: <em>"blue drop"</em> will match less than <em>blue drop</em>.</li>
<li>You can require or exclude terms using + and -: <em>big +blue drop</em> will require a match on <em>blue</em> while <em>big blue -drop</em> will exclude results that contain <em>drop</em>.</li>
</ul>');
}
/**
* Execute a search results based on keyword, filter, and sort strings.
*
* @param $keys
* @param $filterstring
* @param $solrsort
* @param $base_path
* For constructing filter and sort links. Leave empty unless the links need to point somewhere
* other than the base path of the current request.
* @param integer $page
* For pagination.
* @param $caller
* @return Apache_Solr_Response $response
* @throws Exception
*/
function apachesolr_search_execute($keys, $filterstring, $solrsort, $base_path = '', $page = 0, $caller = 'apachesolr_search') {
$params = array();
// This is the object that knows about the query coming from the user.
$query = apachesolr_drupal_query($keys, $filterstring, $solrsort, $base_path);
if (empty($query)) {
throw new Exception(t('Could not construct a Solr query in function apachesolr_search_search()'));
}
$params += apachesolr_search_basic_params($query);
if ($keys) {
$params += apachesolr_search_highlighting_params($query);
$params += apachesolr_search_spellcheck_params($query);
}
else {
// No highlighting, use the teaser as a snippet.
$params['fl'] .= ',teaser';
}
if (module_exists('upload')) {
$params['fl'] .= ',is_upload_count';
}
apachesolr_search_add_facet_params($params, $query);
apachesolr_search_add_boost_params($params, $query, apachesolr_get_solr());
list($final_query, $response) = apachesolr_do_query($caller, $query, $params, $page);
apachesolr_has_searched(TRUE);
// Add search terms and filters onto the breadcrumb.
// We use the original $query to avoid exposing, for example, nodeaccess
// filters in the breadcrumb.
drupal_set_breadcrumb(array_merge(drupal_get_breadcrumb(), $query
->get_breadcrumb()));
return apachesolr_process_response($response, $final_query, $params);
}
function apachesolr_search_basic_params($query) {
$params = array(
'fl' => 'id,nid,title,comment_count,type,created,changed,score,path,url,uid,name',
'rows' => variable_get('apachesolr_rows', 10),
'facet' => 'true',
'facet.mincount' => 1,
'facet.sort' => 'true',
);
return $params;
}
/**
* Add highlighting settings to the search params.
*
* These settings are set in solrconfig.xml.
* See the defaults there.
* If you wish to override them, you can via settings.php
*/
function apachesolr_search_highlighting_params($query) {
$params['hl'] = variable_get('apachesolr_hl_active', NULL);
$params['hl.fragsize'] = variable_get('apachesolr_hl_textsnippetlength', NULL);
$params['hl.simple.pre'] = variable_get('apachesolr_hl_pretag', NULL);
$params['hl.simple.post'] = variable_get('apachesolr_hl_posttag', NULL);
$params['hl.snippets'] = variable_get('apachesolr_hl_numsnippets', NULL);
$params['hl.fl'] = variable_get('apachesolr_hl_fieldtohighlight', NULL);
return $params;
}
function apachesolr_search_spellcheck_params($query) {
$params = array();
if (variable_get('apachesolr_search_spellcheck', FALSE)) {
//Add new parameter to the search request
$params['spellcheck.q'] = $query
->get_query_basic();
$params['spellcheck'] = 'true';
}
return $params;
}
function apachesolr_search_add_facet_params(&$params, $query) {
$facet_query_limits = variable_get('apachesolr_facet_query_limits', array());
$facet_missing = variable_get('apachesolr_facet_missing', array());
foreach (apachesolr_get_enabled_facets() as $module => $module_facets) {
if (!module_exists($module)) {
// When modules are disabled their facet settings may remain.
continue;
}
foreach ($module_facets as $delta => $facet_field) {
// TODO: generalize handling of date and range facets.
if ($module == 'apachesolr_search' && ($facet_field == 'created' || $facet_field == 'changed')) {
list($start, $end, $gap) = apachesolr_search_date_range($query, $facet_field);
if ($gap) {
$params['facet.date'][] = $facet_field;
$params['f.' . $facet_field . '.facet.date.start'] = $start;
$params['f.' . $facet_field . '.facet.date.end'] = $end;
$params['f.' . $facet_field . '.facet.date.gap'] = $gap;
}
}
else {
$params['facet.field'][] = $facet_field;
// Facet limits
if (isset($facet_query_limits[$module][$delta])) {
$params['f.' . $facet_field . '.facet.limit'] = $facet_query_limits[$module][$delta];
}
// Facet missing
if (!empty($facet_missing[$module][$delta])) {
$params['f.' . $facet_field . '.facet.missing'] = 'true';
}
}
}
}
if (!empty($params['facet.field'])) {
// Add a default limit for fields where no limit was set.
$params['facet.limit'] = variable_get('apachesolr_facet_query_limit_default', 20);
}
}
function apachesolr_search_add_boost_params(&$params, $query, $solr) {
// Note - we have query fields set in solrconfig.xml, which will operate when
// none are set.
$qf = variable_get('apachesolr_search_query_fields', array());
$fields = $solr
->getFields();
if ($qf && $fields) {
foreach ($fields as $field_name => $field) {
if (!empty($qf[$field_name])) {
if ($field_name == 'body') {
// Body is the only normed field.
$qf[$field_name] *= 40.0;
}
$params['qf'][] = $field_name . '^' . $qf[$field_name];
}
}
}
$data = $solr
->getLuke();
if (isset($data->index->numDocs)) {
$total = $data->index->numDocs;
}
else {
$total = db_result(db_query("SELECT COUNT(nid) FROM {node}"));
}
// For the boost functions for the created timestamp, etc we use the
// standard date-biasing function, as suggested (but steeper) at
// http://wiki.apache.org/solr/DisMaxRequestHandler
// rord() returns 1 for the newset doc, and the number in the index for
// the oldest doc. The function is thus: $total/(rord()*$steepness + $total).
$date_settings = variable_get('apachesolr_search_date_boost', '4:200.0');
list($date_steepness, $date_boost) = explode(':', $date_settings);
if ($date_boost) {
$params['bf'][] = "recip(rord(created),{$date_steepness},{$total},{$total})^{$date_boost}";
}
// Boost on comment count.
$comment_settings = variable_get('apachesolr_search_comment_boost', '0:0');
list($comment_steepness, $comment_boost) = explode(':', $comment_settings);
if ($comment_boost) {
$params['bf'][] = "recip(rord(comment_count),{$comment_steepness},{$total},{$total})^{$comment_boost}";
}
// Boost for a more recent comment or node edit.
$changed_settings = variable_get('apachesolr_search_changed_boost', '0:0');
list($changed_steepness, $changed_boost) = explode(':', $changed_settings);
if ($changed_boost) {
$params['bf'][] = "recip(rord(last_comment_or_change),{$changed_steepness},{$total},{$total})^{$changed_boost}";
}
// Boost for nodes with sticky bit set.
$sticky_boost = variable_get('apachesolr_search_sticky_boost', 0);
if ($sticky_boost) {
$params['bq'][] = "sticky:true^{$sticky_boost}";
}
// Boost for nodes with promoted bit set.
$promote_boost = variable_get('apachesolr_search_promote_boost', 0);
if ($promote_boost) {
$params['bq'][] = "promote:true^{$promote_boost}";
}
// Modify the weight of results according to the node types.
$type_boosts = variable_get('apachesolr_search_type_boosts', array());
if (!empty($type_boosts)) {
foreach ($type_boosts as $type => $boost) {
// Only add a param if the boost is != 0 (i.e. > "Normal").
if ($boost) {
$params['bq'][] = "type:{$type}^{$boost}";
}
}
}
}
function apachesolr_process_response($response, $query, $params) {
$results = array();
// We default to getting snippets from the body.
$hl_fl = isset($params['hl.fl']) ? $params['hl.fl'] : 'body';
$total = $response->response->numFound;
apachesolr_pager_init($total, $params['rows']);
if ($total > 0) {
foreach ($response->response->docs as $doc) {
$extra = array();
// Find the nicest available snippet.
if (isset($response->highlighting->{$doc->id}->{$hl_fl})) {
$snippet = theme('apachesolr_search_snippets', $doc, $response->highlighting->{$doc->id}->{$hl_fl});
}
elseif (isset($doc->teaser)) {
$snippet = theme('apachesolr_search_snippets', $doc, array(
truncate_utf8($doc->teaser, 256, TRUE),
));
}
else {
$snippet = '';
}
if (!isset($doc->body)) {
$doc->body = $snippet;
}
$doc->created = strtotime($doc->created);
$doc->changed = strtotime($doc->changed);
// Allow modules to alter each document.
drupal_alter('apachesolr_search_result', $doc);
// Copy code from comment_nodeapi().
$extra[] = format_plural($doc->comment_count, '1 comment', '@count comments');
// Copy code from upload_nodeapi()
if (isset($doc->is_upload_count)) {
$extra[] = format_plural($doc->is_upload_count, '1 attachment', '@count attachments');
}
$fields = (array) $doc;
$results[] = array(
'link' => url($doc->path, array(
'absolute' => TRUE,
)),
'type' => apachesolr_search_get_type($doc->type),
// template_preprocess_search_result() runs check_plain() on the title
// again. Decode to correct the display.
'title' => htmlspecialchars_decode($doc->title, ENT_QUOTES),
'user' => theme('username', $doc),
'date' => $doc->changed,
'node' => $doc,
'extra' => $extra,
'score' => $doc->score,
'snippet' => $snippet,
'fields' => $fields,
);
}
// Hook to allow modifications of the retrieved results
foreach (module_implements('apachesolr_process_results') as $module) {
$function = $module . '_apachesolr_process_results';
$function($results);
}
}
return $results;
}
function apachesolr_search_date_range($query, $facet_field) {
foreach ($query
->get_filters($facet_field) as $filter) {
// If we had an ISO date library we could use ISO dates
// directly. Instead, we convert to Unix timestamps for comparison.
// Only use dates if we are able to parse into timestamps.
$start = strtotime($filter['#start']);
$end = strtotime($filter['#end']);
if ($start && $end && $start < $end) {
$start_iso = $filter['#start'];
$end_iso = $filter['#end'];
// Determine the drilldown gap for this range.
$gap = apachesolr_date_gap_drilldown(apachesolr_date_find_query_gap($start_iso, $end_iso));
}
}
// If there is no $delta field in query object, get initial
// facet.date.* params from the DB and determine the best search
// gap to use. This callback assumes $delta is 'changed' or 'created'.
if (!isset($start_iso)) {
$start_iso = apachesolr_date_iso(db_result(db_query("SELECT MIN({$facet_field}) FROM {node} WHERE status = 1")));
// Subtract one second, so that this range's $end_iso is not equal to the
// next range's $start_iso.
$end_iso = apachesolr_date_iso(db_result(db_query("SELECT MAX({$facet_field}) FROM {node} WHERE status = 1")) - 1);
$gap = apachesolr_date_determine_gap($start_iso, $end_iso);
}
// Return a query range from the beginning of a gap period to the beginning
// of the next gap period. We ALWAYS generate query ranges of this form
// and the apachesolr_date_*() helper functions require it.
return array(
"{$start_iso}/{$gap}",
"{$end_iso}+1{$gap}/{$gap}",
"+1{$gap}",
);
}
/**
* Implementation of hook_apachesolr_facets().
*
* Returns an array keyed by block delta.
*/
function apachesolr_search_apachesolr_facets() {
$facets = array();
$facets['type'] = array(
'info' => t('Apache Solr Search: Filter by content type'),
'facet_field' => 'type',
);
$facets['uid'] = array(
'info' => t('Apache Solr Search: Filter by author'),
'facet_field' => 'uid',
);
$facets['language'] = array(
'info' => t('Apache Solr Search: Filter by language'),
'facet_field' => 'language',
);
$facets['changed'] = array(
'info' => t('Apache Solr Search: Filter by updated date'),
'facet_field' => 'changed',
);
$facets['created'] = array(
'info' => t('Apache Solr Search: Filter by post date'),
'facet_field' => 'created',
);
// A book module facet.
if (module_exists('book')) {
$facets['is_book_bid'] = array(
'info' => t('Apache Solr Search: Filter by Book'),
'facet_field' => 'is_book_bid',
);
}
// Get taxonomy vocabulary facets.
if (module_exists('taxonomy')) {
$vocabs = taxonomy_get_vocabularies();
foreach ($vocabs as $vid => $vocab) {
// In this case the delta and facet field are the same.
$delta = 'im_vid_' . $vid;
$facets[$delta] = array(
'info' => t('Apache Solr Search: Filter by taxonomy @name', array(
'@name' => $vocab->name,
)),
'facet_field' => $delta,
);
}
}
// Get CCK field facets.
$fields = apachesolr_cck_fields();
if ($fields) {
foreach ($fields as $name => $field) {
if (!empty($field['facets'])) {
// $delta can only be 32 chars, and the CCK field name may be this
// long also, so we cannot add anything to it.
$facets[$field['field_name']] = array(
'info' => t('Apache Solr Search: Filter by @field', array(
'@field' => $field['widget']['label'],
)),
'facet_field' => apachesolr_index_key($field),
);
}
}
}
return $facets;
}
/**
* Implementation of hook_block().
*/
function apachesolr_search_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$enabled_facets = apachesolr_get_enabled_facets('apachesolr_search');
$facets = apachesolr_search_apachesolr_facets();
// Add the blocks
$blocks = array();
foreach ($enabled_facets as $delta => $facet_field) {
if (isset($facets[$delta])) {
$blocks[$delta] = $facets[$delta] + array(
'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE,
);
}
}
$blocks['currentsearch'] = array(
'info' => t('Apache Solr Search: Current search'),
'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)) {
return;
}
$query = apachesolr_current_query();
$facets = apachesolr_get_enabled_facets('apachesolr_search');
if (empty($facets[$delta]) && $delta != 'currentsearch') {
return;
}
// Handle taxonomy vocabulary facets
if (strpos($delta, 'im_vid_') === 0) {
return apachesolr_search_taxonomy_facet_block($response, $query, $delta);
}
switch ($delta) {
case 'currentsearch':
return apachesolr_search_currentsearch_block($response, $query);
case 'is_book_bid':
return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by book'), 'apachesolr_search_get_book');
case 'language':
return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by language'), 'apachesolr_search_language_name');
case 'uid':
return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by author'), 'apachesolr_search_get_username');
case 'type':
return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by type'), 'apachesolr_search_get_type');
case 'changed':
return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by modification date'));
case 'created':
return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by post date'));
default:
if ($fields = apachesolr_cck_fields()) {
foreach ($fields as $name => $field) {
if ($field['field_name'] == $delta) {
$index_key = apachesolr_index_key($field);
return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $index_key, t('Filter by @field', array(
'@field' => $field['label'],
)));
}
}
}
}
break;
}
break;
case 'configure':
if ($delta != 'currentsearch') {
return apachesolr_facetcount_form('apachesolr_search', $delta);
}
break;
case 'save':
if ($delta != 'currentsearch') {
apachesolr_facetcount_save($edit);
}
break;
}
}
/**
* Generate a list including the field and all its children.
*/
function apachesolr_search_collect_children($field) {
$remove[] = $field;
if (!empty($field['#children'])) {
foreach ($field['#children'] as $child_field) {
$remove = array_merge($remove, apachesolr_search_collect_children($child_field));
}
}
return $remove;
}
/**
* Generate the facet block for a taxonomy vid delta.
*/
function apachesolr_search_taxonomy_facet_block($response, $query, $delta) {
$vid = substr($delta, 7);
if (!module_exists('taxonomy') || !is_numeric($vid)) {
return;
}
// Check that we have a response and a valid vid.
if (is_object($response->facet_counts->facet_fields->{$delta}) && ($vocab = taxonomy_vocabulary_load($vid))) {
$reflect_hierarchy = apachesolr_search_get_hierarchical_vocabularies();
$contains_active = FALSE;
$facets = array();
foreach ($response->facet_counts->facet_fields->{$delta} as $tid => $count) {
// TODO - for now we don't handle facet missing.
if ($tid != '_empty_') {
$active = $query
->has_filter('tid', $tid);
if ($active) {
$contains_active = TRUE;
}
$facets[$tid] = array(
'#name' => 'tid',
'#value' => $tid,
'#exclude' => FALSE,
'#count' => $count,
'#parent' => 0,
'#children' => array(),
'#has_children' => FALSE,
'#active' => $active,
);
}
}
if ($facets && $reflect_hierarchy[$vocab->vid]) {
$placeholders = db_placeholders($facets);
$tids = array_keys($facets);
// @todo: faster as 2x separate queries?
$result = db_query("SELECT tid, parent FROM {term_hierarchy} WHERE parent > 0 AND (tid IN ({$placeholders}) OR parent IN ({$placeholders}))", array_merge($tids, $tids));
while ($term = db_fetch_object($result)) {
// Mark all terms that are parents for later CSS class.
// We assume data in the Solr index is complete - potential for some
// breakage here.
if (isset($facets[$term->parent])) {
$facets[$term->parent]['#has_children'] = TRUE;
if (isset($facets[$term->tid])) {
$facets[$term->tid]['#parent'] = $term->parent;
// Use a reference so we see the updated data.
$facets[$term->parent]['#children'][] =& $facets[$term->tid];
}
}
}
// Check for the case like starting on a taxonomy/term/$tid page
// where parents are not marked as active.
// @todo: can we make this more efficient?
do {
$added_active = FALSE;
foreach ($facets as $tid => $field) {
if ($field['#active'] && $field['#parent'] && !$facets[$field['#parent']]['#active']) {
// This parent has an active child.
$added_active = TRUE;
$query
->add_filter('tid', $field['#parent']);
$facets[$field['#parent']]['#active'] = TRUE;
}
}
} while ($added_active);
foreach ($facets as $tid => $field) {
if (!empty($field['#parent'])) {
// We will render it via its parent.
unset($facets[$tid]);
}
}
}
$items = apachesolr_search_nested_facet_items($query, $facets, $response->response->numFound);
// Process all terms into an item list
if ($items && ($response->response->numFound > 1 || $contains_active)) {
$initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
$limit = isset($initial_limits['apachesolr_search'][$delta]) ? $initial_limits['apachesolr_search'][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
return array(
'subject' => t('Filter by @field', array(
'@field' => $vocab->name,
)),
'content' => theme('apachesolr_facet_list', $items, $limit),
);
}
}
}
/**
* Recursive function that returns a nested array of facet values for use with
* theme_item_list().
*
* @param $query
* The current Solr query.
* @param $facets
* Array of facet items to prepare for rendering, possibly as nested lists.
* @param $num_found
* The number of documents in the current response.
* @param $sort
* If true, the returned list will be sorted based on the count of each
* facets, it's text representation and wither it's active. If false,
* the facets will be returned in the order they were received.
*/
function apachesolr_search_nested_facet_items($query, $facets, $num_found, $sort = TRUE) {
$facet_show_children = variable_get('apachesolr_facet_show_children', array());
$items = array();
foreach ($facets as $field) {
$facet_text = theme('apachesolr_breadcrumb_' . $field['#name'], $field['#value'], $field['#exclude']);
if (!$facet_text) {
$facet_text = $field['#value'];
}
$active = !empty($field['#active']);
$link = array();
$new_query = clone $query;
if ($active) {
foreach (apachesolr_search_collect_children($field) as $child) {
$new_query
->remove_filter($child['#name'], $child['#value']);
}
$options['query'] = $new_query
->get_url_queryvalues();
$link['data'] = theme('apachesolr_unclick_link', $facet_text, $new_query
->get_path(), $options);
}
else {
$new_query
->add_filter($field['#name'], $field['#value']);
$options = array(
'query' => $new_query
->get_url_queryvalues(),
);
$link['data'] = theme('apachesolr_facet_link', $facet_text, $new_query
->get_path(), $options, $field['#count'], FALSE, $num_found);
}
if ($active) {
// '*' sorts before all numbers.
$sortpre = '*';
}
else {
$sortpre = 1000000 - $field['#count'];
}
// Only display children if the block enables it or the parent is clicked.
if (isset($facet_show_children['apachesolr_search'][$field_name]) && $facet_show_children['apachesolr_search'][$field_name] || !empty($field['#children']) && $field['#active'] == TRUE) {
$link['children'] = apachesolr_search_nested_facet_items($query, $field['#children'], $num_found, $sort);
$link['class'] = "expanded-facet";
}
elseif (!empty($field['#has_children'])) {
$link['class'] = "collapsed";
}
$items[$sortpre . '*' . $facet_text . $field['#name'] . $field['#value']] = $link;
}
if ($sort && $items) {
ksort($items);
}
return array_values($items);
}
/**
* Callback function for the 'Filter by book' facet block.
*/
function apachesolr_search_get_book($facet, &$options) {
if (is_numeric($facet)) {
return db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $facet));
}
else {
$options['html'] = TRUE;
return theme('placeholder', t('Not in any book'));
}
}
function apachesolr_search_language_name($lang) {
static $list = NULL;
if (!isset($list)) {
if (function_exists('locale_language_list')) {
$list = locale_language_list();
}
$list['und'] = t('Language neutral');
}
return $lang && isset($list[$lang]) ? $list[$lang] : $lang;
}
/**
* Callback function for the 'Filter by name' facet block.
*/
function apachesolr_search_get_username($facet) {
if (is_numeric($facet)) {
return theme('apachesolr_breadcrumb_uid', $facet);
}
return '';
}
/**
* Callback function for the 'Filter by type' facet block.
*/
function apachesolr_search_get_type($facet) {
$type = node_get_types('name', $facet);
// A disabled or missing node type returns FALSE.
$name = $type === FALSE ? $facet : $type;
return apachesolr_tt("nodetype:type:{$facet}:name", $name);
}
/**
* Implementation of hook_form_[form_id]_alter().
*
* This adds the 0 option to the search admin form.
*/
function apachesolr_search_form_search_admin_settings_alter(&$form, $form_state) {
$form['indexing_throttle']['search_cron_limit']['#options']['0'] = '0';
ksort($form['indexing_throttle']['search_cron_limit']['#options']);
}
/**
* Implementation of hook_form_[form_id]_alter().
*/
function apachesolr_search_form_search_theme_form_alter(&$form, $form_state) {
apachesolr_search_form_search_block_form_alter($form, $form_state);
}
/**
* Implementation of hook_form_[form_id]_alter().
*/
function apachesolr_search_form_search_block_form_alter(&$form, $form_state) {
if (variable_get('apachesolr_search_make_default', 0)) {
$item = menu_get_item();
// Avoid double searches when already on a search page.
if ($item['path'] == 'search/apachesolr_search/%') {
$form['#action'] = url('search/apachesolr_search');
}
if (!isset($form['#submit'])) {
$form['#submit'] = array(
'apachesolr_search_search_box_form_submit',
);
}
else {
$key = array_search('search_box_form_submit', $form['#submit']);
if ($key !== FALSE) {
// Replace the search module's function.
$form['#submit'][$key] = 'apachesolr_search_search_box_form_submit';
}
}
}
}
/**
* Process a block search form submission.
*
* @see search_box_form_submit()
*/
function apachesolr_search_search_box_form_submit($form, &$form_state) {
// The search form relies on control of the redirect destination for its
// functionality, so we override any static destination set in the request,
// for example by drupal_access_denied() or drupal_not_found()
// (see http://drupal.org/node/292565).
if (isset($_REQUEST['destination'])) {
unset($_REQUEST['destination']);
}
if (isset($_REQUEST['edit']['destination'])) {
unset($_REQUEST['edit']['destination']);
}
$form_id = $form['form_id']['#value'];
$keys = $form_state['values'][$form_id];
// Handle Apache webserver clean URL quirks.
if (variable_get('clean_url', '0')) {
$keys = str_replace('+', '%2B', $keys);
}
$form_state['redirect'] = 'search/apachesolr_search/' . trim($keys);
}
/**
* Implementation of hook_form_[form_id]_alter().
*
* This adds spelling suggestions, retain filters to the search form.
*/
function apachesolr_search_form_search_form_alter(&$form, $form_state) {
if ($form['module']['#value'] == 'apachesolr_search') {
$form['#submit'] = array(
'apachesolr_search_form_search_submit',
);
// No other modification make sense unless a query is active.
// Note - this means that the query must always be run before
// calling drupal_get_form('search_form').
$apachesolr_has_searched = apachesolr_has_searched();
$queryvalues = array();
if ($apachesolr_has_searched) {
$query = apachesolr_current_query();
$queryvalues = $query
->get_url_queryvalues();
}
$form['basic']['apachesolr_search']['#tree'] = TRUE;
$form['basic']['apachesolr_search']['queryvalues'] = array(
'#type' => 'hidden',
// We use JSON encoding instead of PHP serialize, since otherwise we be
// at risk of user input being injected into the hidden string and
// unserialized.
'#default_value' => json_encode($queryvalues),
);
$form['basic']['apachesolr_search']['get'] = array(
'#type' => 'hidden',
'#default_value' => json_encode(array_diff_key($_GET, array(
'q' => 1,
'page' => 1,
'filters' => 1,
'solrsort' => 1,
'retain-filters' => 1,
))),
);
if ($queryvalues || isset($form_state['post']['apachesolr_search']['retain-filters'])) {
$form['basic']['apachesolr_search']['retain-filters'] = array(
'#type' => 'checkbox',
'#title' => t('Retain current filters'),
'#default_value' => (int) isset($_GET['retain-filters']),
);
}
if (variable_get('apachesolr_search_spellcheck', FALSE) && $apachesolr_has_searched && ($response = apachesolr_static_response_cache())) {
// Get spellchecker suggestions into an array.
if (isset($response->spellcheck->suggestions) && $response->spellcheck->suggestions) {
$suggestions = get_object_vars($response->spellcheck->suggestions);
if ($suggestions) {
// Get the original query and replace words.
$query = apachesolr_current_query();
foreach ($suggestions as $word => $value) {
$replacements[$word] = $value->suggestion[0];
}
$new_keywords = strtr($query
->get_query_basic(), $replacements);
// Show only if suggestion is different than current query.
if ($query
->get_query_basic() != $new_keywords) {
$form['basic']['suggestion'] = array(
'#value' => theme('apachesolr_search_suggestions', array(
l($new_keywords, $query
->get_path($new_keywords)),
)),
);
}
}
}
}
}
}
/**
* Added form submit function to account for Apache mode_rewrite quirks.
*
* @see apachesolr_search_form_search_form_alter()
*/
function apachesolr_search_form_search_submit($form, &$form_state) {
$fv = $form_state['values'];
$keys = $fv['processed_keys'];
$base = 'search/' . $fv['module'] . '/';
if (variable_get('clean_url', '0')) {
$keys = str_replace('+', '%2B', $keys);
}
$get = json_decode($fv['apachesolr_search']['get'], TRUE);
$queryvalues = json_decode($fv['apachesolr_search']['queryvalues'], TRUE);
if (!empty($fv['apachesolr_search']['retain-filters']) && $queryvalues) {
$get = $queryvalues + $get;
$get['retain-filters'] = '1';
}
$form_state['redirect'] = array(
$base . $keys,
$get,
);
if ($keys == '' && !$queryvalues) {
form_set_error('keys', t('Please enter some keywords.'));
}
}
/**
* Implementation of hook_form_[form_id]_alter().
*
* This adds options to the apachesolr admin form.
*/
function apachesolr_search_form_apachesolr_settings_alter(&$form, $form_state) {
$form['advanced']['apachesolr_search_make_default'] = array(
'#type' => 'radios',
'#title' => t('Make Apache Solr Search the default'),
'#default_value' => variable_get('apachesolr_search_make_default', 0),
'#options' => array(
0 => t('Disabled'),
1 => t('Enabled'),
),
'#description' => t('Hides core node search, and makes the search block submit to Apache Solr Search'),
);
$form['advanced']['apachesolr_search_default_previous'] = array(
'#type' => 'value',
'#value' => variable_get('apachesolr_search_make_default', 0),
);
$form['advanced']['apachesolr_search_taxonomy_links'] = array(
'#type' => 'radios',
'#title' => t('Use Apache Solr for taxonomy links'),
'#default_value' => variable_get('apachesolr_search_taxonomy_links', 0),
'#description' => t('Note: Vocabularies that need this behavior need to be checked off on the <a href="@enabled_filters_url">enabled filters</a> settings page. Note that content types ommitted from the Apache Solr index will not be shown.', array(
'@enabled_filters_url' => url('admin/settings/apachesolr/enabled-filters'),
)),
'#options' => array(
0 => t('Disabled'),
1 => t('Enabled'),
),
);
$form['advanced']['apachesolr_search_taxonomy_previous'] = array(
'#type' => 'value',
'#value' => variable_get('apachesolr_search_taxonomy_links', 0),
);
$form['apachesolr_search_spellcheck'] = array(
'#type' => 'checkbox',
'#title' => t('Enable spellchecker and suggestions'),
'#default_value' => variable_get('apachesolr_search_spellcheck', FALSE),
'#description' => t('Enable spellchecker and get word suggestions. Also known as the "Did you mean ... ?" feature.'),
);
$form['#submit'][] = 'apachesolr_search_build_spellcheck';
$form['#submit'][] = 'apachesolr_search_make_default_submit';
// Move buttons to the bottom.
$buttons = $form['buttons'];
unset($form['buttons']);
$form['buttons'] = $buttons;
}
/**
* Form submit funtion - do a menu rebuild if needed.
*
* @see apachesolr_search_form_apachesolr_settings_alter()
*/
function apachesolr_search_make_default_submit($form, &$form_state) {
// We use variable_get() instead of the form values so as to also handle reset to defaults.
if ($form_state['values']['apachesolr_search_default_previous'] != variable_get('apachesolr_search_make_default', 0) || $form_state['values']['apachesolr_search_taxonomy_previous'] != variable_get('apachesolr_search_taxonomy_links', 0)) {
// Take account of path changes
menu_rebuild();
}
}
/**
* Implementation of hook_form_[form_id]_alter().
*
* Rebuild (empty) the spellcheck dictionary when the index is deleted..
*/
function apachesolr_search_form_apachesolr_delete_index_form_alter(&$form, $form_state) {
$form['submit']['#submit'][] = 'apachesolr_search_build_spellcheck';
}
function apachesolr_search_build_spellcheck() {
try {
$solr = apachesolr_get_solr();
$params['spellcheck'] = 'true';
$params['spellcheck.build'] = 'true';
$response = $solr
->search('solr', 0, 0, $params);
} catch (Exception $e) {
watchdog('Apache Solr', nl2br(check_plain($e
->getMessage())), NULL, WATCHDOG_ERROR);
}
}
/**
* Implementation of hook_theme().
*/
function apachesolr_search_theme() {
return array(
'apachesolr_breadcrumb_is_book_bid' => array(
'arguments' => array(
'bid' => NULL,
'exclude' => FALSE,
),
),
'apachesolr_breadcrumb_uid' => array(
'arguments' => array(
'uid' => NULL,
'exclude' => FALSE,
),
),
'apachesolr_breadcrumb_tid' => array(
'arguments' => array(
'tid' => NULL,
'exclude' => FALSE,
),
),
'apachesolr_breadcrumb_type' => array(
'arguments' => array(
'type' => NULL,
'exclude' => FALSE,
),
),
'apachesolr_breadcrumb_language' => array(
'arguments' => array(
'lang' => NULL,
'exclude' => FALSE,
),
),
'apachesolr_breadcrumb_changed' => array(
'arguments' => array(
'type' => NULL,
),
),
'apachesolr_breadcrumb_created' => array(
'arguments' => array(
'type' => NULL,
),
),
'apachesolr_currentsearch' => array(
'arguments' => array(
'total_found' => NULL,
'links' => NULL,
),
),
'apachesolr_search_snippets' => array(
'arguments' => array(
'doc' => NULL,
'snippets' => NULL,
),
),
'apachesolr_search_results_page' => array(
'arguments' => array(
'form' => '',
'results' => '',
),
),
'apachesolr_search_suggestions' => array(
'arguments' => array(
'links' => NULL,
),
),
);
}
function theme_apachesolr_breadcrumb_language($lang, $exclude = FALSE) {
return apachesolr_search_language_name($lang);
}
/**
* Proxy theme function for 'created' and 'changed' date fields.
*/
function theme_apachesolr_breadcrumb_date_range($range) {
if (preg_match('@[\\[\\{](\\S+) TO (\\S+)[\\]\\}]@', $range, $match)) {
return apachesolr_date_format_range($match[1], $match[2]);
}
return $range;
}
function theme_apachesolr_breadcrumb_changed($range) {
return theme_apachesolr_breadcrumb_date_range($range);
}
function theme_apachesolr_breadcrumb_created($range) {
return theme_apachesolr_breadcrumb_date_range($range);
}
/**
* Return the username from $uid
*/
function theme_apachesolr_breadcrumb_uid($uid) {
if ($uid == 0) {
return variable_get('anonymous', t('Anonymous'));
}
else {
return db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $uid));
}
}
/**
* Return the term name from $tid, or $tid as a fallback.
*/
function theme_apachesolr_breadcrumb_tid($tid) {
if (function_exists('taxonomy_get_term')) {
if ($term = taxonomy_get_term($tid)) {
return $term->name;
}
}
return $tid;
}
/**
* Return the human readable text for a content type.
*/
function theme_apachesolr_breadcrumb_type($type) {
$name = node_get_types('name', $type);
return apachesolr_tt("nodetype:type:{$type}:name", $name);
}
/**
* Return the title of a book.
*/
function theme_apachesolr_breadcrumb_is_book_bid($bid) {
if (is_numeric($bid)) {
return db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $bid));
}
else {
return t('Not in any book');
}
}
/**
* Return current search block contents
*/
function theme_apachesolr_currentsearch($total_found, $links) {
return theme_item_list($links, format_plural($total_found, 'Search found 1 item', 'Search found @count items'));
}
/**
* Theme the highlighted snippet text for a search entry.
*
* @param object $doc
* @param array $snippets
*
*/
function theme_apachesolr_search_snippets($doc, $snippets) {
return implode(' ... ', $snippets) . ' ...';
}
/**
* Theme function for the search results page.
*/
function theme_apachesolr_search_results_page($form, $results) {
return $form . $results;
}
/**
* Default theme function for spelling suggestions.
*/
function theme_apachesolr_search_suggestions($links) {
$output = '<div class="spelling-suggestions">';
$output .= '<dl class="form-item"><dt><strong>' . t('Did you mean') . '</strong></dt>';
foreach ((array) $links as $link) {
$output .= '<dd>' . $link . '</dd>';
}
$output .= '</dl></div>';
return $output;
}
function apachesolr_get_parent_terms($tids) {
// Find the starting tid terms and then all their parents.
$parent_terms = array();
$new_tids = $tids;
do {
$result = db_query(db_rewrite_sql("SELECT th.tid, th.parent FROM {term_hierarchy} th WHERE th.tid IN (" . db_placeholders($new_tids) . ")", 'th', 'tid'), $new_tids);
$new_tids = array();
while ($term = db_fetch_object($result)) {
$parent_terms[$term->tid] = $term;
if ($term->parent > 0) {
$new_tids[] = $term->parent;
}
}
} while ($new_tids);
return $parent_terms;
}
/**
* Return the contents of the "Current search" block.
*
* @param $response
* The Solr response object.
* @param $query
* The Solr query object.
*/
function apachesolr_search_currentsearch_block($response, $query) {
$fields = $query
->get_filters();
$links = array();
$facets = array();
// If current search has keys, offer current search without them
if ($keys = $query
->get_query_basic()) {
$links[] = theme('apachesolr_unclick_link', $keys, $query
->get_path(''), array(
'query' => $query
->get_url_queryvalues(),
));
}
// Find all taxonomy terms to be treated in a hierarchy.
if (module_exists('taxonomy')) {
$reflect_hierarchy = apachesolr_search_get_hierarchical_vocabularies();
$facets = array();
foreach ($fields as $index => $field) {
if ($field['#name'] && 'tid' == $field['#name']) {
$term = taxonomy_get_term($field['#value']);
if ($reflect_hierarchy[$term->vid]) {
$fields[$index] += array(
'#parent' => 0,
'#children' => array(),
);
// Just save the index for later lookup.
$facets[$term->tid] = $index;
}
}
}
if ($facets) {
// Get all term hierarchy information.
$all_terms = apachesolr_get_parent_terms(array_keys($facets));
foreach ($all_terms as $tid => $term) {
if (!isset($facets[$tid])) {
// This is a parent that is missing from the query. E.g. we started
// on a taxonomy/term/$tid page.
$query
->add_filter('tid', $tid);
// Ordering is wonky, but oh well...
$fields[] = array(
'#name' => 'tid',
'#value' => $tid,
'#exclude' => FALSE,
'#parent' => 0,
'#children' => array(),
);
// Get the index of the newly added facet.
end($fields);
$facets[$tid] = key($fields);
}
}
foreach ($all_terms as $tid => $term) {
$index = $facets[$term->tid];
if (isset($facets[$term->parent])) {
// Use a reference so we see the updated data.
$fields[$facets[$term->parent]]['#children'][] =& $fields[$index];
$fields[$index]['#parent'] = $term->parent;
}
}
}
}
// We don't directly render any items with a parent.
foreach ($fields as $index => $field) {
$fields[$index]['#active'] = TRUE;
if (!empty($fields[$index]['#parent']) || !$field['#name']) {
// We will render it via its parent.
unset($fields[$index]);
}
}
$links = array_merge($links, apachesolr_search_nested_facet_items($query, $fields, $response->response->numFound, FALSE));
if ($links) {
$content = theme('apachesolr_currentsearch', $response->response->numFound, $links);
return array(
'subject' => t('Current search'),
'content' => $content,
);
}
}
/**
* Return an array of taxonomy facets that should be displayed hierarchically.
*/
function apachesolr_search_get_hierarchical_vocabularies() {
static $result;
if (!isset($result)) {
$result = array();
if (function_exists('taxonomy_get_vocabularies')) {
$vocabularies = taxonomy_get_vocabularies();
$force_flat = variable_get('apachesolr_search_force_flat_vocabularies', array());
foreach ($vocabularies as $voc) {
// If the vocabulary is not multiple-parent hierarchical and not
// freetagging and not designated to be forced to display flat.
if ($voc->hierarchy != 2 && $voc->tags != 1 && empty($force_flat[$voc->vid])) {
$result[$voc->vid] = 1;
}
}
}
}
return $result;
}