'Apache Solr search integration',
'description' => 'Administer Apache Solr.',
'page callback' => 'drupal_get_form',
'page arguments' => array('apachesolr_settings'),
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
);
$items['admin/config/search/apachesolr/server/%apachesolr_server/edit'] = array(
'title' => 'Apache Solr search server edit',
'description' => 'Edit Apache Solr server.',
'page callback' => 'drupal_get_form',
'page arguments' => array('apachesolr_server_edit_form', 5),
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
);
$items['admin/config/search/apachesolr/server/%apachesolr_server/delete'] = array(
'title' => 'Apache Solr search server delete',
'page callback' => 'apachesolr_server_delete_page',
'page arguments' => array(5),
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
);
$items['admin/config/search/apachesolr/server/add'] = array(
'title' => 'Apache Solr search server add',
'description' => 'Add Apache Solr server.',
'page callback' => 'drupal_get_form',
'page arguments' => array('apachesolr_server_edit_form', NULL),
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
);
$items['admin/config/search/apachesolr/settings'] = array(
'title' => 'Settings',
'weight' => -10,
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/config/search/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,
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
);
$items['admin/config/search/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/config/search/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/config/search/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',
'description' => 'Infromation about the contents of the index the server',
'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/config/search/apachesolr/mlt/add_block'] = array(
'title' => 'Add a new content recommendation block',
'page callback' => 'drupal_get_form',
'page arguments' => array('apachesolr_mlt_add_block_form'),
'access arguments' => array('administer search'),
'file' => 'apachesolr.admin.inc',
'type' => MENU_LOCAL_ACTION,
);
$items['admin/config/search/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;
}
/**
* Implements hook_init().
*/
function apachesolr_init() {
if (arg(0) == 'admin') {
// Add the CSS for this module
drupal_add_css(drupal_get_path('module', 'apachesolr') . '/apachesolr.css');
}
}
/**
* 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)));
// TODO - apachesolr_server_variable_get()
drupal_goto('search/node/' . drupal_encode_path($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;
// Set a random 6 digit base-36 number as the hash.
$hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6);
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;
}
/**
* Implements hook_user().
*
* Mark nodes as needing re-indexing if the author name changes.
*/
function apachesolr_user_update(&$edit, $account, $category) {
if (isset($edit['name']) && $account->name != $edit['name']) {
// TODO: performance issue. see http://drupal.org/node/592522
$nid_query = db_select('node')->fields('node', array('nid'))->where("uid = :uid", array(':uid' => $account->uid));
db_update('apachesolr_search_node')->condition('nid', $nid_query, 'IN')->fields(array('changed' => REQUEST_TIME))->execute();
}
}
/**
* Implements hook_taxonomy().
*
* Mark nodes as needing re-indexing if a term name changes.
*/
function apachesolr_taxonomy_term_update($term) {
// TODO: performance issue. see http://drupal.org/node/592522
$nid_query = db_select('taxonomy_index')->fields('taxonomy_index', array('nid'))->where("tid = :tid", array(':tid' => $term->tid));
db_update('apachesolr_search_node')->condition('nid', $nid_query, 'IN')->fields(array('changed' => REQUEST_TIME))->execute();
// TODO: the rest, such as term deletion.
}
/**
* Implement hook_comment_*().
*
* Mark nodes as needing re-indexing if comments are added or changed.
* Like search_comment().
*/
/**
* Implements hook_comment_insert()
*/
function apachesolr_comment_insert($comment){
apachesolr_mark_node($comment->nid);
}
/**
* Implements hook_comment_update()
*/
function apachesolr_comment_update($comment){
apachesolr_mark_node($comment->nid);
}
/**
* Implements hook_comment_delete()
*/
function apachesolr_comment_delete($comment){
apachesolr_mark_node($comment->nid);
}
/**
* Implements hook_comment_publish()
*/
function apachesolr_comment_publish($comment){
apachesolr_mark_node($comment->nid);
}
/**
* Implements hook_comment_unpublish()
*/
function apachesolr_comment_unpublish($comment){
apachesolr_mark_node($comment->nid);
}
/**
* Mark one node as needing re-indexing.
*/
function apachesolr_mark_node($nid) {
db_update('apachesolr_search_node')->condition('nid', $nid)->fields(array('changed' => REQUEST_TIME))->execute();
}
/**
* Implements of hook_node_type_update().
*/
function apachesolr_node_type_update($info) {
if (!empty($info->old_type) && $info->old_type != $info->type) {
// We cannot be sure we are going before or after node module.
$nid = db_select('node')->fields('node', array('nid'))->where("type = :new OR type = :old", array(':new' => $info->type, ':old' => $info->old_type));
db_update('apachesolr_search_node')->condition('nid', $nid, 'IN')->fields(array('changed' => REQUEST_TIME))->execute();
}
}
/**
* Helper function for modules implmenting hook_search's 'status' op.
*/
function apachesolr_index_status($namespace) {
$excluded_types = apachesolr_get_excluded_types($namespace);
list($last_change, $last_nid) = apachesolr_get_last_change($namespace);
$query = db_select('apachesolr_search_node', 'asn')->condition('asn.status', 1);
apachesolr_query_add_excluded_types($query, $excluded_types);
$total = $query->countQuery()->execute()->fetchField();
$query = db_select('apachesolr_search_node', 'asn')
->condition('asn.status', 1)
->condition(db_or()->condition('asn.changed', $last_change, '>')->condition(db_and()->condition('asn.changed', $last_change)->condition('asn.nid', $last_nid, '>')));
apachesolr_query_add_excluded_types($query, $excluded_types);
$remaining = $query->countQuery()->execute()->fetchField();
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)) {
$sel_query = db_select('node')
->fields('node', array('nid'))
->condition('type', $type);
db_delete('apachesolr_search_node')->condition('nid', $sel_query, 'IN')->execute();
// Populate table
$sel_query = db_select('node')
->fields('node', array('nid', 'status'))
->condition('type', $type);
$sel_query->addExpression(REQUEST_TIME, 'changed');
db_insert('apachesolr_search_node')->fields(array('nid', 'status', 'changed'))
->from($sel_query)
->execute();
}
else {
db_delete('apachesolr_search_node')->execute();
// Populate table
$sel_query = db_select('node', 'n')
->fields('n', array('nid', 'status'));
$insert_fields = array('nid', 'status', 'changed');
// Check if the module is enabled, if so use the timestamp from the latest comment
if (module_exists('comment')) {
$sel_query->leftJoin('node_comment_statistics', 'c', 'n.nid = c.nid');
$sel_query->addExpression('GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0))', 'changed');
}
else {
$sel_query->addExpression('GREATEST(n.created, n.changed)', 'changed');
}
db_insert('apachesolr_search_node')->fields($insert_fields)
->from($sel_query)
->execute();
// Make sure no nodes end up with a timestamp that's in the future.
db_update('apachesolr_search_node')->condition('changed', REQUEST_TIME, '>')->fields(array('changed' => REQUEST_TIME))->execute();
apachesolr_clear_last_index();
}
cache_clear_all('*', 'cache_apachesolr', TRUE);
}
function apachesolr_get_excluded_types($namespace) {
$excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace);
if ($excluded_types) {
$excluded_types = array_unique($excluded_types);
}
return $excluded_types;
}
function apachesolr_query_add_excluded_types($query, $excluded_types){
if($excluded_types){
$query->innerJoin('node', 'n', 'n.nid = asn.nid');
$query->condition('n.type', $excluded_types, 'NOT IN');
}
}
function apachesolr_get_last_change($namespace) {
extract(apachesolr_get_last_index($namespace));
return array($last_change, $last_nid);
}
/**
* 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', APACHESOLR_READ_WRITE) == APACHESOLR_READ_ONLY) {
return $rows;
}
$excluded_types = apachesolr_get_excluded_types($namespace);
list($last_change, $last_nid) = apachesolr_get_last_change($namespace);
//TODO: remove old code
//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 > :last_changed OR (asn.changed = :last_changed AND asn.nid > :last_nid)) AND asn.status = 1 ". $exclude_sql ."ORDER BY asn.changed ASC, asn.nid ASC", 0, $limit, $args);
$query = db_select('apachesolr_search_node', 'asn')
->fields('asn', array('nid', 'changed'))
->condition('asn.status', 1)
->condition(db_or()->condition('asn.changed', $last_change, '>')->condition(db_and()->condition('asn.changed', $last_change)->condition('asn.nid', $last_nid, '>')))
->orderBy('asn.changed', 'ASC')
->orderBy('asn.nid', 'ASC')
->range(0, $limit);
apachesolr_query_add_excluded_types($query, $excluded_types);
$result = $query->execute();
return $result;
}
/**
* 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;
}
module_load_include('inc', 'apachesolr', 'apachesolr.index');
$documents = array();
$old_position = apachesolr_get_last_index($namespace);
$position = $old_position;
$position['nodes_processed'] = 0;
// 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');
// Always build the content for the index as an anonynmous user.
global $user;
drupal_save_session(FALSE);
$saved_user = $user;
$user = drupal_anonymous_user();
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 - log the error.
watchdog('Apache Solr', 'Error constructing documents to index:
!message', array('!message' => "Node ID: {$row->nid}
" . nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR);
}
$position['nodes_processed']++;
}
// Restore the user.
$user = $saved_user;
drupal_save_session(TRUE);
// 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(REQUEST_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(REQUEST_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);
}
/**
* Implements hook_cron().
*/
function apachesolr_cron() {
try {
$solr = apachesolr_get_solr();
// Check for unpublished content that wasn't deleted from the index.
$query = db_select('apachesolr_search_node', 'asn');
$n_alias = $query->innerJoin('node', 'n', 'n.nid = asn.nid');
$query->fields($n_alias, array('nid', 'status'));
$query->condition('asn.status', "$n_alias.status", '<>');
$result = $query->execute();
foreach ($result as $node) {
apachesolr_node_update($node, FALSE);
}
// Check for deleted content that wasn't deleted from the index.
$query = db_select('apachesolr_search_node', 'asn');
$query->fields('asn', array('nid'));
$n_alias = $query->leftJoin('node', 'n', 'n.nid = asn.nid');
$query->isNull("$n_alias.nid");
$result = $query->execute();
foreach ($result as $node) {
apachesolr_node_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 = REQUEST_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);
}
}
/**
* Implements hook_flush_caches().
*/
function apachesolr_flush_caches() {
return array('cache_apachesolr');
}
/**
* A wrapper for cache_clear_all to be used as a submit handler on forms that
* require clearing Luke cache etc.
*/
function apachesolr_clear_cache($server_id) {
try {
$solr = apachesolr_get_solr($server_id);
$solr->clearCache();
}
catch (Exception $e) {
watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
drupal_set_message(nl2br(check_plain($e->getMessage())), 'warning');
}
}
/**
* Implements hook_node_insert().
*/
function apachesolr_node_insert($node) {
// Make sure no node ends up with a timestamp that's in the future by using
// REQUEST_TIME rather than the node's changed or created timestamp.
db_insert('apachesolr_search_node')->fields(array('nid' => $node->nid, 'status' => $node->status, 'changed' => REQUEST_TIME))->execute();
}
/**
* Implements hook_node_delete().
*/
function apachesolr_node_delete($node, $set_message = TRUE) {
if (apachesolr_delete_node_from_index($node)) {
// There was no exception, so delete from the table.
db_delete('apachesolr_search_node')->condition('nid', $node->nid)->execute();
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.');
}
}
}
/**
* Implements hook_node_update().
*/
function apachesolr_node_update($node, $set_message = TRUE) {
// Check if the node has gone from published to unpublished.
if (!$node->status && db_select('apachesolr_search_node', 'asn')->fields('asn', array('status'))->condition('nid', $node->nid)->execute()) {
if (apachesolr_delete_node_from_index($node)) {
// There was no exception, so update the table.
db_update('apachesolr_search_node')->condition('nid', $node->nid)->fields(array('changed' => REQUEST_TIME, 'status' => $node->status))->execute();
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_update('apachesolr_search_node')->condition('nid', $node->nid)->fields(array('changed' => REQUEST_TIME, 'status' => $node->status))->execute();
}
}
/**
* 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($server_id, $module = NULL) {
$enabled = apachesolr_server_variable_get($server_id, '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($server_id, $enabled) {
apachesolr_server_variable_set($server_id, '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($server_id, $module, $facets = array()) {
$enabled = apachesolr_server_variable_get('apachesolr_enabled_facets', array());
if (!empty($facets) && is_array($facets)) {
$enabled[$module] = $facets;
}
else {
unset($enabled[$module]);
}
apachesolr_server_variable_set($server_id, 'apachesolr_enabled_facets', $enabled);
return $enabled;
}
/**
* Implements hook_block_info().
*/
function apachesolr_block_info() {
// 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' => DRUPAL_CACHE_PER_PAGE,
);
return $blocks;
}
/**
* Implements hook_block_view().
*/
function apachesolr_block_view($delta = '') {
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', array('docs' => $docs, 'delta' => $delta));
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;
if ($name == 'score') {
$direction = '';
$new_direction = 'desc'; // We only sort by descending score.
}
elseif ($active) {
$direction = $toggle[$solrsort['#direction']];
$new_direction = $toggle[$solrsort['#direction']];
}
else {
$direction = '';
$new_direction = $sort['default'];
}
$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', array(
'text' => $link['title'],
'path' => $link['path'],
'options' => $link['options'],
'active' => $link['active'],
'direction' => $link['direction']));
}
return array(
'subject' => t('Sort by'),
'content' => theme('apachesolr_sort_list', array('items' => $themed_links))
);
}
}
/**
* Implements hook_block_configure().
*/
function apachesolr_block_configure($delta = '') {
if ($delta != 'sort') {
require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
return apachesolr_mlt_block_form($delta);
}
}
/**
* Implements hook_block_save().
*/
function apachesolr_block_save($delta = '', $edit = array()) {
if ($delta != 'sort') {
require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
apachesolr_mlt_save_block($edit, $delta);
}
}
/**
* 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(DrupalSolrQueryInterface $query, $module, $delta) {
// TYPE HIERARCHY.
// If the block is configured to heed type hierarchy 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 (!isset($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 based on the Solr
* index key (field name).
*
* @param string $field
* The field_name being sought.
*/
function apachesolr_get_facet_definition_by_field_name($index_key) {
$definitions = apachesolr_get_facet_definitions();
foreach ($definitions as $module => $facets) {
if (isset($definitions[$module][$index_key])) {
return $definitions[$module][$index_key];
}
}
}
/**
* Implements hook_form_[form_id]_alter().
*
* Make sure to flush cache when content types are changed.
*/
function apachesolr_form_node_type_form_alter(&$form, $form_state) {
$form['#submit'][] = 'apachesolr_clear_cache';
}
/**
* Implements hook_form_[form_id]_alter(). (D7)
*
* Make sure to flush cache when fields are added.
*/
function apachesolr_form_field_ui_field_overview_form_alter(&$form, $form_state) {
$form['#submit'][] = 'apachesolr_clear_cache';
}
/**
* Implements hook_form_[form_id]_alter(). (D7)
*
* Make sure to flush cache when fields are updated.
*/
function apachesolr_form_field_ui_field_edit_form_alter(&$form, $form_state) {
$form['#submit'][] = 'apachesolr_clear_cache';
}
/**
* Implements 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(apachesolr_default_server());
// 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.
*
* The $delta param is the Solr index field name.
*/
function apachesolr_facet_block($response, DrupalSolrQueryInterface $query, $module, $delta, $filter_by, $facet_callback = FALSE) {
if (!empty($response->facet_counts->facet_fields->$delta)) {
$facet_query_sorts = variable_get('apachesolr_facet_query_sorts', array());
$contains_active = FALSE;
$items = array();
foreach ($response->facet_counts->facet_fields->$delta as $facet => $count) {
$options = array('delta' => $delta);
$exclude = FALSE;
// Solr sends this back if it's empty.
if ($facet == '_empty_') {
$exclude = TRUE;
$facet = '[* TO *]';
$options['html'] = TRUE;
}
if ($facet_callback && function_exists($facet_callback)) {
$facet_text = $facet_callback($facet, $options);
}
elseif ($exclude) {
$facet_text = drupal_placeholder(array('text' => t('Missing this field')));
}
else {
$facet_text = $facet;
}
$active = $query->has_filter($delta, $facet);
if ($active) {
// '*' sorts before all numbers.
$sortpre = '*';
}
elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index key') === 0) {
// If this block is to be alphabetically sorted by key, change $sortpre.
$sortpre = $facet;
}
elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index') === 0) {
// If this block is to be alphabetically/numerically sorted by value, change $sortpre.
$sortpre = $facet_text;
}
elseif ($exclude) {
// '-' sorts before all numbers, but after '*'.
$sortpre = '-';
}
else {
$sortpre = 1000000 - $count;
}
$new_query = clone $query;
if ($active) {
$contains_active = TRUE;
$new_query->remove_filter($delta, $facet);
$options['query'] = $new_query->get_url_queryvalues();
$link = theme('apachesolr_unclick_link', array(
'facet_text' => $facet_text,
'path' => $new_query->get_path(),
'options' => $options));
}
else {
$new_query->add_filter($delta, $facet, $exclude);
$options['query'] = $new_query->get_url_queryvalues();
$link = theme('apachesolr_facet_link', array(
'facet_text' => $facet_text,
'path' => $new_query->get_path(),
'options' => $options,
'count' => $count,
'active' => FALSE,
'num_found' => $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)) {
$sort_setting = isset($facet_query_sorts[$module][$delta]) ? $facet_query_sorts[$module][$delta] : 'index asc';
switch ($sort_setting) {
case 'index numeric asc':
ksort($items, SORT_NUMERIC);
break;
case 'index numeric desc':
krsort($items, SORT_NUMERIC);
break;
case 'index desc':
case 'index key desc':
krsort($items, SORT_STRING);
break;
case 'index asc':
case 'index key asc':
default:
ksort($items, SORT_STRING);
break;
}
// 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', array(
'items' => $items,
'display_limit' => $limit,
'delta' => $delta,
));
$render = array(
'output' => array(
'#markup' => $output,
),
'#contextual_links' => array(
'enabled_filters' => array('admin/config/search/apachesolr/enabled-filters', array($delta)),
),
);
return array('subject' => $filter_by, 'content' => $render);
}
}
return NULL;
}
/**
* Helper function for displaying a date facet block.
*
* TODO: Refactor with apachesolr_facet_block().
*/
function apachesolr_date_facet_block($response, DrupalSolrQueryInterface $query, $module, $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', array(
'facet_text' => $facet_text,
'path' => $new_query->get_path(),
'options' => $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);
if (isset($field->start)) {
$start = $field->start;
unset($field->start);
}
// 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', array(
'facet_text' => $facet_text,
'path' => $new_query->get_path(),
'options' => $options,
'count' => $count,
'active' => FALSE,
'num_found' => $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][$facet_field]) ? $initial_limits[$module][$facet_field] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
$output = theme('apachesolr_facet_list', array(
'items' => $items,
'display_limit' => $limit,
'delta' => $facet_field,
));
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 !== FALSE) {
switch ($gap) {
case 'YEAR':
return format_date($unix, 'custom', 'Y', 'UTC');
case 'MONTH':
return format_date($unix, 'custom', 'F Y', 'UTC');
case 'DAY':
return format_date($unix, 'custom', 'F j, Y', 'UTC');
case 'HOUR':
return format_date($unix, 'custom', 'g A', 'UTC');
case 'MINUTE':
return format_date($unix, 'custom', 'g:i A', 'UTC');
case 'SECOND':
return format_date($unix, 'custom', 'g:i:s A', 'UTC');
}
}
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('Ym', $start) != date('Ym', $end)) {
return 'MONTH';
}
if ($end - $start > 86400) {
return 'DAY';
}
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());
$children = variable_get('apachesolr_facet_show_children', 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('Alphabetical, ascending'),
'index desc' => t('Alphabetical, descending'),
'index numeric asc' => t('Numeric, ascending'),
'index numeric desc' => t('Numeric, descending'),
'index key asc' => t('Key sort, ascending'),
'index key desc' => t('Key sort, 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. %Alphabetical will sort alphabetically, and %Numeric numerically, either ascending or descending.', array(
'%Count' => t('Count'),
'%Alphabetical' => t('Alphanumeric'),
'%Numeric' => t('Numeric'),
)),
'#default_value' => isset($sorts[$module][$delta]) ? $sorts[$module][$delta] : 'count',
);
}
$form['apachesolr_facet_show_children'] = array(
'#type' => 'radios',
'#title' => t('Always show child facets'),
'#options' => array(0 => t('No'), 1 => t('Yes')),
'#description' => t('Show the child facets even if the parent facet is not selected.'),
'#default_value' => isset($children[$module][$delta]) ? $children[$module][$delta] : 0,
);
$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);
$children = variable_get('apachesolr_facet_show_children', array());
$children[$module][$delta] = (int)$edit['apachesolr_facet_show_children'];
variable_set('apachesolr_facet_show_children', $children);
$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 object.
*
* Example:
*
* function my_module_apachesolr_modify_query($query, $caller) {
* // I only want to see articles by the admin!
* $query->add_filter("uid", 1);
*
* }
*
* @return
* TRUE to abort the current query, FALSE otherwise.
*/
function apachesolr_modify_query(DrupalSolrQueryInterface $query, $caller) {
if (empty($query)) {
// This should only happen if Solr is not set up - avoids fatal errors.
return TRUE;
}
// 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';
if ($function_name($query, $caller)) {
// Someone pulled the ejection handle.
return TRUE;
}
}
// TODO: make this process internal to the query object.
if ($fq = $query->get_fq()) {
foreach ($fq as $delta => $values) {
if (is_array($values) || is_object($values)) {
foreach ($values as $value) {
$query->params['fq'][$delta][] = $value;
}
}
}
}
// Add sort if present.
// TODO: make this process internal to the query object.
if ($sort = $query->get_solrsort()) {
$sortstring = $sort['#name'] .' '. $sort['#direction'];
// We don't bother telling Solr to do its default sort.
if ($sortstring != 'score desc') {
$query->params['sort'] = $sortstring;
}
}
// Do not abort the query.
return FALSE;
}
/**
* 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;
}
/**
* Get or set the default server ID for the current page.
*/
function apachesolr_default_server($server_id = NULL) {
static $default_server_id = NULL;
if (isset($server_id)) {
$default_server_id = $server_id;
}
if (empty($default_server_id)) {
$default_server_id = variable_get('apachesolr_default_server', 'solr');
}
return $default_server_id;
}
/**
* Factory method for solr singleton objects. Structure allows for an arbitrary
* number of solr objects to be used based on a name whie maps to
* the host, port, path combination.
* Get an instance like this:
* $solr = apachesolr_get_solr();
*
* @throws Exception
*/
function apachesolr_get_solr($id = NULL) {
$solr_cache = &drupal_static(__FUNCTION__);
$servers = apachesolr_load_all_servers();
if (empty($id) || empty($servers[$id])) {
$id = apachesolr_default_server();
}
$host = $servers[$id]['host'];
$port = $servers[$id]['port'];
$path = $servers[$id]['path'];
$class = $servers[$id]['service_class'];
if (empty($solr_cache[$id])) {
// Use the default class if none is specified.
if (empty($class)) {
$class = variable_get('apachesolr_service_class', 'DrupalApacheSolrService');
}
// Takes advantage of auto-loading.
$solr = new $class($id, $host, $port, $path);
$solr_cache[$id] = $solr;
}
return $solr_cache[$id];
}
function apachesolr_load_all_servers() {
$servers = &drupal_static(__FUNCTION__);
if (isset($servers)) {
return $servers;
}
// Use cache_get to avoid DB when using memcache, etc.
$cache = cache_get('apachesolr:servers', 'cache_apachesolr');
if (isset($cache->data)) {
$servers = $cache->data;
}
else {
$servers = db_query('SELECT * FROM {apachesolr_server}')->fetchAllAssoc('server_id', PDO::FETCH_ASSOC);
foreach ($servers as $id => &$server) {
$conf = db_query('SELECT name, value FROM {apachesolr_server_variable} WHERE server_id = :server_id', array('server_id' => $id))->fetchAllAssoc('name', PDO::FETCH_ASSOC);
foreach ($conf as $name => $data) {
$server['conf'][$name] = unserialize($data['value']);
}
}
cache_set('apachesolr:servers', $servers, 'cache_apachesolr');
}
return $servers;
}
function apachesolr_server_load($server_id) {
$servers = apachesolr_load_all_servers();
return isset($servers[$server_id]) ? $servers[$server_id] : FALSE;
}
function apachesolr_server_delete($server_id) {
$result = db_delete('apachesolr_server')
->condition('server_id', $server_id)
->execute();
$result = db_delete('apachesolr_server')
->condition('server_id', $server_id)
->execute();
cache_clear_all('apachesolr:servers', 'cache_apachesolr');
drupal_static_reset('apachesolr_load_all_servers');
drupal_static_reset('apachesolr_get_solr');
return $result;
}
function apachesolr_server_save($server, $original_id = NULL) {
$default = array('server_id' => NULL, 'name' => '', 'scheme' => NULL, 'host' => '', 'port' => '', 'path' => '', 'service_class' => '');
$conf = isset($server['conf']) ? $server['conf'] : array();
// Remove any unexpected fields.
// @todo - getthis from the schema, or maybe use drupal_write_record().
$server = array_intersect_key($server, $default);
$server_id = $server['server_id'];
if (empty($original_id) || $server['server_id'] == $original_id) {
unset($server['server_id']);
db_merge('apachesolr_server')
->fields($server)
->key(array('server_id' => $server_id))
->execute();
}
else {
db_update('apachesolr_server')
->fields($server)
->condition('server_id', $original_id)
->execute();
// Update the server_id column for the per-server variables.
db_update('apachesolr_server_variable')
->fields(array('server_id' => $server_id))
->condition('server_id', $original_id)
->execute();
}
// Update the server variables (if any). Note that a change in server_id
// will already have happened above.
foreach ($conf as $name => $value) {
db_merge('apachesolr_server_variable')
->key(array('server_id' => $server_id, 'name' => $name))
->fields(array('value' => serialize($value)))
->execute();
}
cache_clear_all('apachesolr:servers', 'cache_apachesolr');
drupal_static_reset('apachesolr_load_all_servers');
drupal_static_reset('apachesolr_get_solr');
}
/**
* Get a named variable, or return the default.
*
* @see variable_get()
*/
function apachesolr_server_variable_get($server_id, $name, $default = NULL) {
$server = apachesolr_server_load($server_id);
if (isset($server['conf'][$name])) {
return $server['conf'][$name];
}
return $default;
}
/**
* Set a named variable, or return the default.
*
* @see variable_set()
*/
function apachesolr_server_variable_set($server_id, $name, $value) {
db_merge('apachesolr_server_variable')
->key(array('server_id' => $server_id, 'name' => $name))
->fields(array('value' => serialize($value)))
->execute();
cache_clear_all('apachesolr:servers', 'cache_apachesolr');
drupal_static_reset('apachesolr_load_all_servers');
drupal_static_reset('apachesolr_get_solr');
}
/**
* Get a named variable, or return the default.
*
* @see variable_del()
*/
function apachesolr_server_variable_del($server_id, $name) {
db_delete('apachesolr_server_variable')
->condition('server_id', $server_id)
->condition('name', $name)
->execute();
cache_clear_all('apachesolr:servers', 'cache_apachesolr');
drupal_static_reset('apachesolr_load_all_servers');
drupal_static_reset('apachesolr_get_solr');
}
/**
* 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, $port, $path, $class = NULL) {
if (empty($class)) {
$class = variable_get('apachesolr_service_class', 'DrupalApacheSolrService');
}
$ping = FALSE;
try {
// Takes advantage of auto-loading.
$solr = new $class('', $host, $port, $path);
$ping = @$solr->ping(variable_get('apachesolr_ping_timeout', 4));
}
catch (Exception $e) {
watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
}
return $ping;
}
/**
* Execute a search based on a query object.
*
* Normally this function is used with the default (dismax) handler for keyword
* searches. The $final_query that's returned will have been modified by
* both hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().
*
* @param $caller
* String, name of the calling module or function for use as a cache namespace.
* @param $current_query
* A query object from apachesolr_drupal_query(). It will be modified by
* hook_apachesolr_prepare_query() and then cached in apachesolr_current_query().
* @param $page
* For paging into results, using $current_query->params['rows'] results per page.
*
* @return array($final_query, $response)
*
* @throws Exception
*/
function apachesolr_do_query($caller, DrupalSolrQueryInterface $current_query, $page = 0) {
// Allow modules to alter the query prior to statically caching it.
// This can e.g. be used to add available sorts.
foreach (module_implements('apachesolr_prepare_query') as $module) {
$function_name = $module . '_apachesolr_prepare_query';
$function_name($current_query, $caller);
}
// Cache the original query. Since all the built queries go through
// this process, all the hook_invocations will happen later
$query = apachesolr_current_query($current_query, $caller);
// This hook allows modules to modify the query and params objects.
$eject = apachesolr_modify_query($query, $caller);
// A module implementing hook_apachesolr_modify_query() aborted the search.
if ($eject) {
return array(NULL, array());
}
$query->params['start'] = $page * $query->params['rows'];
// Final chance for the caller to modify the query. The signature
// is: CALLER_finalize_query($query);
$function = $caller . '_finalize_query';
if (function_exists($function)) {
$function($query);
}
$keys = $query->get_query_basic();
if ($keys == '' && isset($query->params['fq'])) {
// Move the fq params to q.alt for better performance.
$qalt = array();
foreach ($query->params['fq'] as $delta => $value) {
// Move the fq param if it has no local params and is not negative.
if (!preg_match('/^(?:\{!|-)/', $value)) {
$qalt[] = '(' . $value . ')';
unset($query->params['fq'][$delta]);
}
}
if ($qalt) {
$query->params['q.alt'] = implode(' ', $qalt);
}
}
// We must run htmlspecialchars() here since converted entities are in the index.
// and thus bare entities &, > or < won't match.
$response = $query->search(htmlspecialchars($keys, ENT_QUOTES, 'UTF-8'));
// The response is cached so that it is accessible to the blocks and anything
// else that needs it beyond the initial search.
apachesolr_static_response_cache($response, $caller);
return array($query, $response);
}
/**
* 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.
*
* @todo reverse the order of parameters in future branches.
*/
function apachesolr_static_response_cache($response = NULL, $namespace = 'apachesolr_search') {
static $_response = array();
if (is_object($response)) {
$_response[$namespace] = clone $response;
}
if (!isset($_response[$namespace])) {
$_response[$namespace] = NULL;
}
return $_response[$namespace];
}
/**
* 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 DrupalApacheSolrService.
*
* @return
* DrupalSolrQueryInterface object.
*
* @throws Exception
*/
function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '', $solr = NULL) {
$class = variable_get('apachesolr_query_class', 'SolrBaseQuery');
if (empty($solr)) {
$solr = apachesolr_get_solr();
}
return new $class($solr, $keys, $filters, $solrsort, $base_path);
}
/**
* Static getter/setter for the current query
*
* @todo reverse the order of parameters in future branches.
*/
function apachesolr_current_query(DrupalSolrQueryInterface $query = NULL, $namespace = 'apachesolr_search') {
static $saved_query = array();
if (is_object($query)) {
$saved_query[$namespace] = clone $query;
}
return is_object($saved_query[$namespace]) ? clone $saved_query[$namespace] : NULL;
}
/**
* Construct a dynamic index name based on information about a field.
*
* 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 'tint':
$type_prefix = 'ti';
break;
case 'tfloat':
$type_prefix = 'tf';
break;
case 'tdouble':
$type_prefix = 'tp';
break;
default:
$type_prefix = 's'; // String
}
$sm = $field['multiple'] ? 'm_' : 's_';
// Block deltas are limited to 32 chars.
return substr($type_prefix . $sm . $field['name'], 0, 32);
}
/**
* 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, H5, 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['tm_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_entity_fields('node') as $field_nm => $field_info) {
$map[apachesolr_index_key($field_info)] = t('Field of type @type: %label', array('@type' => $field_info['field']['type'], '%label' => $field_info['display_name']));
}
drupal_alter('apachesolr_field_name_map', $map);
}
return isset($map[$field_name]) ? $map[$field_name] : $field_name;
}
/**
* Returns array containing information about node fields that should be indexed
*/
function apachesolr_entity_fields($entity_type = 'node') {
static $fields = array();
$fields = &drupal_static(__FUNCTION__, array());
if (!isset($fields[$entity_type])) {
$fields[$entity_type] = array();
$mappings = module_invoke_all('apachesolr_field_mappings');
foreach (array_keys($mappings) as $key) {
// Set all values with defaults.
$mappings[$key] += array(
'display_callback' => '',
'indexing_callback' => '',
'index_type' => 'string',
'facet_block_callback' => '',
'name_callback' => '',
// Field API allows any field to be multi-valued.
'multiple' => TRUE,
);
}
// Allow other modules to add or alter mappings.
drupal_alter('apachesolr_field_mappings', $mappings);
$modules = system_get_info('module');
$instances = field_info_instances($entity_type);
foreach (field_info_fields() as $field_name => $field) {
$row = array();
if (isset($field['bundles'][$entity_type]) && (isset($mappings['per-field'][$field_name]) || isset($mappings[$field['type']]))) {
// Find the mapping.
if (isset($mappings['per-field'][$field_name])) {
$row = $mappings['per-field'][$field_name];
}
else {
$row = $mappings[$field['type']];
}
// The field info array.
$row['field'] = $field;
// Since we use the index key as the block delta in apachesolr_search, we need a name
// to build whatever the index key is that is used for faceting.
// @todo: for fields like taxonomy we are indexing multiple Solr fields
// per entity field, but are keying on a single Solr field name here.
$function = $row['name_callback'];
if ($function && function_exists($function)) {
$row['name'] = $function($field);
}
else {
$row['name'] = $field['id'] . '_' . $field['field_name'];
}
$row['module_name'] = $modules[$field['module']]['name'];
// Set display name
$display_name = array();
foreach ($field['bundles'][$entity_type] as $bundle) {
if (empty($instances[$bundle][$field_name]['display']['search_index']) || $instances[$bundle][$field_name]['display']['search_index'] != 'hidden') {
$row['display_name'] = $instances[$bundle][$field_name]['label'];
$row['bundles'][] = $bundle;
}
}
// Only add to the $fields array if some instances are displayed for the search index.
if (!empty($row['bundles'])) {
// Use the Solr index key as the array key.
$fields[$entity_type][apachesolr_index_key($row)] = $row;
}
}
}
}
return $fields[$entity_type];
}
/**
* Strip html tags and also control characters that cause Jetty/Solr to fail.
*/
function apachesolr_clean_text($text) {
// Add spaces before stripping tags to avoid running words together.
$text = filter_xss(str_replace(array('<', '>'), array(' <', '> '), $text), array());
// Decode entities and then make safe any < or > characters.
return htmlspecialchars(html_entity_decode($text, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8');
}
/**
* Use the list.module's list_allowed_values() 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_fields_list_display_callback($facet, $options) {
// @see list_field_formatter_view()
$fields = field_info_fields();
$field_name = $options['delta'];
if (isset($fields[$field_name])) {
$allowed_values = list_allowed_values($field);
if (isset($allowed_values[$facet])) {
$output = field_filter_xss($allowed_values[$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;
}
}
/**
* Generalizes calling quazi-method callbacks on entities.
*
* The callbacks are defined in the entity's info array definition.
*
* @param stdClass $entity
* The entity on which to call this quasi-method.
* @param string $entity_type
* The type of entity we have. Drupal doesn't tell us this automatically.
* @param string $callback
* The name of the callback we want to call.
* @param array $arguments
* @return mixed
* NULL if the callback wasn't found. If it was, whatever that callback returns
* is returned.
*/
function apachesolr_entity_callback($entity, $entity_type, $callback, $arguments = array()) {
$info = entity_get_info($entity_type);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$callback_function = apachesolr_entity_get_callback($entity_type, $callback, $bundle);
if (function_exists($callback_function)) {
return call_user_func_array($callback_function, $arguments);
}
return NULL;
}
/**
* Returns the callback function appropriate for a given entity type/bundle.
*
* @param string $entity_type
* The entity type for which we want to know the approprite callback.
* @param string $callback
* The callback for which we want the appropriate function.
* @param string $bundle
* If specified, the bundle of the entity in question. Some callbacks may
* be overridden on a bundle-level. Not specified only the entity-level
* callback will be checked.
* @return string
* The function name for this callback, or NULL if not specified.
*/
function apachesolr_entity_get_callback($entity_type, $callback, $bundle = NULL) {
$info = entity_get_info($entity_type);
// A bundle-specific callback takes precedence over the generic one for the
// entity type.
if ($bundle && isset($info['bundles'][$bundle]['apachesolr'][$callback])) {
$callback_function = $info['bundles'][$bundle]['apachesolr'][$callback];
}
elseif (isset($info['apachesolr'][$callback])) {
$callback_function = $info['apachesolr'][$callback];
}
else {
$callback_function = NULL;
}
return $callback_function;
}
/**
* Determines if we should index the provided entity.
*
* Whether or not a given entity is indexed is determined on a per-bundle basis.
* Entities/Bundles that have no index flag are presumed to not get indexed.
*
* @param stdClass $entity
* The entity we may or may not want to index.
* @param string $type
* The type of entity.
* @return boolean
* TRUE if this entity should be indexed, FALSE otherwise.
*/
function apachesolr_entity_should_index($entity, $type) {
$info = entity_get_info($type);
list($id, $vid, $bundle) = entity_extract_ids($type, $entity);
if ($bundle && isset($info['bundles'][$bundle]['apachesolr']['index']) && $info['bundles'][$bundle]['apachesolr']['index']) {
return TRUE;
}
return FALSE;
}
/**
* Implements 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(
'variables' => 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(
'variables' => 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(
'variables' => array('items' => NULL, 'display_limit' => 0, 'delta' => ''),
),
/**
* Returns a list of links generated by apachesolr_sort_link
*/
'apachesolr_sort_list' => array(
'variables' => array('items' => NULL),
),
/**
* Returns a link which can be used to search the results.
*/
'apachesolr_sort_link' => array(
'variables' => 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(
'variables' => array('docs' => NULL, 'delta' => NULL),
),
);
}
/**
* A per-server version of system_settings_form().
*/
function apachesolr_settings_form($form, $server_id) {
$form['#server_id'] = $server_id;
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save configuration'),
'#submit' => array('apachesolr_settings_form_submit'),
);
$form['actions']['reset'] = array(
'#type' => 'submit',
'#value' => t('Reset to defaults'),
'#submit' => array('apachesolr_settings_form_reset'),
);
return $form;
}
function apachesolr_settings_form_submit($form, &$form_state) {
// Exclude unnecessary elements.
form_state_values_clean($form_state);
foreach ($form_state['values'] as $key => $value) {
if (is_array($value) && isset($form_state['values']['array_filter'])) {
$value = array_keys(array_filter($value));
}
// There is no need to set default variable values.
if (!isset($form[$key]['#default_value']) || is_array($value) || $form[$key]['#default_value'] != $value) {
apachesolr_server_variable_set($form['#server_id'], $key, $value);
}
}
drupal_set_message(t('The configuration options have been saved.'));
}
function apachesolr_settings_form_reset($form, &$form_state) {
// Exclude unnecessary elements.
form_state_values_clean($form_state);
foreach ($form_state['values'] as $key => $value) {
apachesolr_server_variable_del($form['#server_id'], $key);
}
drupal_set_message(t('The configuration options have been reset to their default values.'));
}
/**
* 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, $solr = NULL) {
try {
$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',
);
// We can optionally specify a Solr object.
$query = apachesolr_drupal_query('id:' . $id, '', '', '', $solr);
$query->params = array(
'qt' => 'mlt',
'fl' => 'nid,title,path,url',
'mlt.fl' => implode(',', $settings['mlt_fl']),
'start' => 0,
'rows' => $settings['num_results'],
);
foreach ($fields as $form_key => $name) {
if (!empty($settings[$form_key])) {
$query->params[$name] = $settings[$form_key];
}
}
$type_filters = array();
if (is_array($settings['mlt_type_filters'])) {
foreach ($settings['mlt_type_filters'] as $type_filter) {
$type_filters[] = "type:{$type_filter}";
}
$query->params['fq']['mlt'][] = '(' . implode(' OR ', $type_filters) . ') ';
}
if ($custom_filters = $settings['mlt_custom_filters']) {
$query->params['fq']['mlt'][] = $custom_filters;
}
// This hook allows modules to modify the query object.
$eject = apachesolr_modify_query($query, 'apachesolr_mlt');
if ($eject) {
return;
}
$response = $query->search();
if ($response->response) {
$docs = (array) end($response->response);
return $docs;
}
}
catch (Exception $e) {
watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
}
}
/**
* Implements 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/config/search/apachesolr/mlt/delete_block/'. $block['delta']['#value']));
}
}
}
/**
* Implements hook_hook_info().
*/
function apachesolr_hook_info() {
$hooks['apachesolr_field_mappings'] = array(
'group' => 'apachesolr',
);
$hooks['apachesolr_field_mappings_alter'] = array(
'group' => 'apachesolr',
);
return $hooks;
}
/**
* Implements hook_apachesolr_field_mappings().
*/
function field_apachesolr_field_mappings() {
$mappings = array(
'list_integer' => array(
'display_callback' => 'apachesolr_fields_list_display_callback',
'indexing_callback' => 'apachesolr_fields_list_indexing_callback',
'index_type' => 'string',
),
'list_float' => array(
'display_callback' => 'apachesolr_fields_list_display_callback',
'indexing_callback' => 'apachesolr_fields_list_indexing_callback',
'index_type' => 'string',
),
'list_text' => array(
'display_callback' => 'apachesolr_fields_list_display_callback',
'indexing_callback' => 'apachesolr_fields_list_indexing_callback',
'index_type' => 'string',
),
'list_boolean' => array(
'display_callback' => 'apachesolr_fields_list_display_callback',
'indexing_callback' => 'apachesolr_fields_list_indexing_callback',
'index_type' => 'boolean',
),
'taxonomy_term_reference' => array(
'display_callback' => 'apachesolr_taxonomy_get_term',
'indexing_callback' => 'apachesolr_term_reference_indexing_callback',
'index_type' => 'integer',
),
);
return $mappings;
}
/**
* Implements hook_apachesolr_field_mappings() on behalf of CCK.
*/
function content_apachesolr_field_mappings() {
$mappings = array(
'node_reference' => array(
'display_callback' => 'apachesolr_cck_nodereference_field_callback',
'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
'index_type' => 'integer',
),
'user_reference' => array(
'display_callback' => 'apachesolr_cck_userreference_field_callback',
'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
'index_type' => 'integer',
),
);
return $mappings;
}
function apachesolr_taxonomy_get_term($tid) {
if (function_exists('taxonomy_term_load')) {
$term = taxonomy_term_load($tid);
return $term->name;
}
}
/**
* 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' => DRUPAL_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($vars) {
$docs = $vars['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', array('items' => $links));
}
function theme_apachesolr_facet_link($vars) {
$vars['options']['attributes']['class'][] = 'apachesolr-facet';
if ($vars['active']) {
$vars['options']['attributes']['class'][] = 'active';
}
$vars['options']['attributes']['class'] = implode(' ', $vars['options']['attributes']['class']);
return apachesolr_l($vars['facet_text'] ." ({$vars['count']})", $vars['path'], $vars['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 (is_null($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();
$show_more_selector = array();
foreach (apachesolr_get_facet_definitions() as $module => $definitions) {
foreach ($definitions as $facet => $facet_definition) {
$facet_map[$facet_definition['facet_field']] = $facet;
}
}
$server_id = apachesolr_default_server();
foreach (apachesolr_get_enabled_facets($server_id) 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($vars) {
apachesolr_js();
if (empty($vars['options']['html'])) {
$vars['facet_text'] = check_plain($vars['facet_text']);
}
else {
// Don't pass this option as TRUE into apachesolr_l().
unset($vars['options']['html']);
}
$vars['options']['attributes']['class'] = 'apachesolr-unclick';
return apachesolr_l("(-)", $vars['path'], $vars['options']) . ' '. $vars['facet_text'];
}
function theme_apachesolr_sort_link($vars) {
$icon = '';
if ($vars['direction']) {
$icon = ' '. theme('tablesort_indicator', array('style' => $vars['direction']));
}
if ($vars['active']) {
if (isset($vars['options']['attributes']['class'])) {
$vars['options']['attributes']['class'] .= ' active';
}
else {
$vars['options']['attributes']['class'] = 'active';
}
}
return $icon . apachesolr_l($vars['text'], $vars['path'], $vars['options']);
}
function theme_apachesolr_facet_list($vars) {
apachesolr_js();
// theme('item_list') expects a numerically indexed array.
$items = array_values($vars['items']);
// If there is a limit and the facet count is over the limit, hide the rest.
if ( ($vars['display_limit'] > 0) && (count($items) > $vars['display_limit'])) {
// Split items array into displayed and hidden.
$hidden_items = array_splice($items, $vars['display_limit']);
foreach ($hidden_items as $hidden_item) {
if (!is_array($hidden_item)) {
$hidden_item = array('data' => $hidden_item);
}
$hidden_item['class'][] = 'apachesolr-hidden-facet';
$items[] = $hidden_item;
}
}
return theme('item_list', array('items' => $items));
}
function theme_apachesolr_sort_list($vars) {
// theme('item_list') expects a numerically indexed array.
$vars['items'] = array_values($vars['items']);
return theme('item_list', array('items' => $vars['items']));
}
/**
* The interface for all 'query' objects.
*/
interface DrupalSolrQueryInterface {
/**
* Get all filters, or the subset of filters for one field.
*
* @param $name
* Optional name of a Solr field.
*
* @return an array of filters.
*/
function get_filters($name = NULL);
/**
* 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
*
* @return TRUE or FALSE
*/
function has_filter($field, $value);
/**
* 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.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function add_filter($field, $value, $exclude = FALSE);
/**
* 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
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function remove_filter($field, $value = NULL);
/**
* Get the query's keywords.
*/
function get_keys();
/**
* Set the query's keywords.
*
* @param string $keys
* The new keywords.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function set_keys($keys);
/**
* Removes the query's keywords.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function remove_keys();
/**
* Return the search path (including the search keywords).
*/
function get_path();
/**
* Return filters and sort in a form suitable for a query param to url().
*/
function get_url_queryvalues();
/**
* Return the basic string query.
*/
function get_query_basic();
/**
* Return the sorts that are provided by the query object.
*
* @return array all the sorts provided
*/
function get_available_sorts();
/**
* Make a sort available.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function set_available_sort($field, $sort);
/**
* Get the solrsort.
*
* Returns the non-urlencode, non-aliased sort field and direction.
* as an array keyed with '#name' and '#direction'.
*/
function get_solrsort();
/**
* Set the solrsort.
*
* @param $field
* The name of a field in the solr index that's an allowed sort.
* @param $direction
* 'asc' or 'desc'
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function set_solrsort($field, $direction);
/**
* Add a subquery to the query.
*
* @param DrupalSolrQueryInterface $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.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function add_subquery(DrupalSolrQueryInterface $query, $fq_operator = 'OR', $q_operator = 'AND');
/**
* Remove a specific subquery.
*
* @param DrupalSolrQueryInterface $query
* the query to remove
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function remove_subquery(DrupalSolrQueryInterface $query);
/**
* Remove all subqueries.
*
* @return DrupalSolrQueryInterface
* The called object.
*/
function remove_subqueries();
/**
* Send a search query using the Solr object passed into the constructor.
*
* @param $keys
* If not set, the query object will supply keywords based on its
* instance variables. The params for the search are taken from
* the public $query->params array.
*
* @return Apache_Solr_Response
* A response object.
*/
function search($keys = NULL);
/**
* Call a method on the Solr object passed into the constructor.
*
* @return
* Any method return.
*/
function solr($method);
}