apachesolr.module

Tracking 5.x-2.x branch
  1. drupal
    1. 5 contributions/apachesolr/apachesolr.module
    2. 6 contributions/apachesolr/apachesolr.module
    3. 7 contributions/apachesolr/apachesolr.module

Integration with the Apache Solr search application.

Functions & methods

NameDescription
apachesolr_blockImplementation of hook_block().
apachesolr_cck_fieldsInvokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
apachesolr_cck_nodereference_field_callbackUse the content.module's content_format() to format the field based on its nid ($facet).
apachesolr_cck_text_field_callbackUse the content.module's content_format() to format the field based on its value ($facet).
apachesolr_cck_userreference_field_callbackUse the content.module's content_format() to format the field based on its uid ($facet).
apachesolr_clear_cck_fields_cacheThis function is invoked after a CCK add/edit/delete and is used to clear the cache.
apachesolr_clear_last_indexClear a specific namespace's last changed and nid, or clear all.
apachesolr_commentImplementation of hook_comment().
apachesolr_cronImplementation of hook_cron().
apachesolr_current_queryStatic getter/setter for the current query
apachesolr_date_determine_gapDetermine the best search gap to use for an arbitrary date range.
apachesolr_date_facet_blockHelper function for displaying a date facet block.
apachesolr_date_find_query_gapDetermine the gap in a date range query filter that we generated.
apachesolr_date_format_iso_by_gapFormat an ISO date string based on the gap used to generate it.
apachesolr_date_format_rangeFormat the beginning of a date range query filter that we generated.
apachesolr_date_gap_drilldownReturn the next smaller date gap.
apachesolr_date_isoConvert date from timestamp into ISO 8601 format. http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
apachesolr_delete_node_from_index
apachesolr_document_idGenerate a unique ID for an entity being indexed.
apachesolr_do_queryExecute a search based on a query object.
apachesolr_drupal_queryFactory function for query objects.
apachesolr_facetcount_formUsed by the 'configure' $op of hook_block so that modules can generically set facet limits on their blocks.
apachesolr_facetcount_saveUsed by the 'save' $op of hook_block so that modules can generically set facet limits on their blocks.
apachesolr_facet_blockHelper function for displaying a facet block.
apachesolr_failureDetermines 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.
apachesolr_field_name_mapTry to map a schema field name to a human-readable description.
apachesolr_form_alterImplementation of hook_form_alter
apachesolr_get_enabled_facetsReturn the enabled facets from the specified block array.
apachesolr_get_last_indexReturns last changed and last nid for an indexing namespace.
apachesolr_get_nodes_to_indexReturns an array of rows from a query based on an indexing namespace.
apachesolr_get_solrFactory method for solr singleton object. Structure allows for an arbitrary number of solr objects to be used based on the host, port, path combination. Get an instance like this: $solr = apachesolr_get_solr();
apachesolr_has_searchedSemaphore that indicates whether a search has been done. Blocks use this later to decide whether they should load or not.
apachesolr_index_keyarray('index_type' => 'integer', 'multiple' => TRUE, 'name' => 'fieldname', ),
apachesolr_index_nodesFunction to handle the indexing of nodes.
apachesolr_index_statusHelper function for modules implementing hook_search's 'status' op.
apachesolr_index_updatedHelper function to keep track of when the index has been updated.
apachesolr_lA replacement for l()
apachesolr_mark_nodeMark one node as needing re-indexing.
apachesolr_menuImplementation of hook_menu().
apachesolr_mlt_list_blocksReturns a list of blocks. Used by hook_block
apachesolr_mlt_load_block
apachesolr_mlt_suggestionsPerforms a moreLikeThis query using the settings and retrieves documents.
apachesolr_modify_queryThis hook allows modules to modify the query and params objects.
apachesolr_nodeapiImplementation of hook_nodeapi().
apachesolr_node_typeImplementation of hook_node_type().
apachesolr_pager_initInitialize a pager for theme('pager') without running an SQL query.
apachesolr_rebuild_index_tableTruncate 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.
apachesolr_save_enabled_facetsSave the enabled facets for all modules.
apachesolr_save_module_facetsSave the enabled facets for one module.
apachesolr_set_stats_messageCall drupal_set_message() with the text.
apachesolr_site_hashLike $site_key in _update_refresh() - returns a site-specific hash.
apachesolr_static_response_cacheIt 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.
apachesolr_taxonomyImplementation of hook_taxonomy().
apachesolr_ttWrapper function for tt() if i18nstrings enabled.
apachesolr_userImplementation of hook_user().
theme_apachesolr_facet_link
theme_apachesolr_facet_list
theme_apachesolr_mlt_recommendation_block
theme_apachesolr_sort_link
theme_apachesolr_sort_list
theme_apachesolr_unclick_link
_apachesolr_exclude_types
_apachesolr_nodeapi_deleteHelper function for hook_nodeapi().
_apachesolr_nodeapi_updateHelper function for hook_nodeapi().

Constants

NameDescription
APACHESOLR_READ_ONLY
APACHESOLR_READ_WRITE

Interfaces

NameDescription
Drupal_Solr_Query_InterfaceThe interface for all 'query' objects.

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Integration with the Apache Solr search application.
  5. */
  6. define('APACHESOLR_READ_WRITE', 0);
  7. define('APACHESOLR_READ_ONLY', 1);
  8. include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.d6.inc');
  9. // Include admin functions only on the admin pages.
  10. if (arg(0) == 'admin' && (arg(1) == 'settings' || arg(1) == 'logs') && arg(2) == 'apachesolr') {
  11. include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
  12. }
  13. /**
  14. * Implementation of hook_menu().
  15. */
  16. function apachesolr_menu($may_cache) {
  17. $items = array();
  18. if ($may_cache) {
  19. $items[] = array(
  20. 'path' => 'admin/settings/apachesolr',
  21. 'title' => t('Apache Solr'),
  22. 'description' => t('Administer Apache Solr.'),
  23. 'callback' => 'drupal_get_form',
  24. 'callback arguments' => 'apachesolr_settings',
  25. 'access' => user_access('administer search'),
  26. );
  27. $items[] = array(
  28. 'path' => 'admin/settings/apachesolr/settings',
  29. 'title' => t('Settings'),
  30. 'weight' => -10,
  31. 'access' => user_access('administer search'),
  32. 'type' => MENU_DEFAULT_LOCAL_TASK,
  33. );
  34. $items[] = array(
  35. 'path' => 'admin/settings/apachesolr/enabled-filters',
  36. 'title' => t('Enabled filters'),
  37. 'callback' => 'drupal_get_form',
  38. 'callback arguments' => 'apachesolr_enabled_facets_form',
  39. 'weight' => -7,
  40. 'access' => user_access('administer search'),
  41. 'type' => MENU_LOCAL_TASK,
  42. );
  43. $items[] = array(
  44. 'path' => 'admin/settings/apachesolr/index',
  45. 'title' => t('Search index'),
  46. 'callback' => 'apachesolr_index_page',
  47. 'access' => user_access('administer search'),
  48. 'weight' => -8,
  49. 'type' => MENU_LOCAL_TASK,
  50. );
  51. $items[] = array(
  52. 'path' => 'admin/settings/apachesolr/index/clear/confirm',
  53. 'title' => t('Confirm the re-indexing of all content'),
  54. 'callback' => 'drupal_get_form',
  55. 'callback arguments' => 'apachesolr_clear_index_confirm',
  56. 'access' => user_access('administer search'),
  57. 'type' => MENU_CALLBACK,
  58. );
  59. $items[] = array(
  60. 'path' => 'admin/settings/apachesolr/index/delete/confirm',
  61. 'title' => t('Confirm index deletion'),
  62. 'callback' => 'drupal_get_form',
  63. 'callback arguments' => 'apachesolr_delete_index_confirm',
  64. 'access' => user_access('administer search'),
  65. 'type' => MENU_CALLBACK,
  66. );
  67. $items[] = array(
  68. 'path' => 'admin/logs/apachesolr',
  69. 'title' => t('Apache Solr search index'),
  70. 'callback' => 'apachesolr_index_report',
  71. 'access' => user_access('access site reports'),
  72. );
  73. $items[] = array(
  74. 'path' => 'admin/logs/apachesolr/index',
  75. 'title' => t('Search index'),
  76. 'type' => MENU_DEFAULT_LOCAL_TASK,
  77. );
  78. $items[] = array(
  79. 'path' => 'admin/settings/apachesolr/mlt/add_block',
  80. 'callback' => 'drupal_get_form',
  81. 'callback arguments' => 'apachesolr_mlt_add_block_form',
  82. 'access' => user_access('administer search'),
  83. 'type' => MENU_CALLBACK,
  84. );
  85. }
  86. else {
  87. if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'apachesolr' && arg(3) == 'mlt' && arg(4) == 'delete_block' && arg(5)) {
  88. $items[] = array(
  89. 'path' => 'admin/settings/apachesolr/mlt/delete_block/'. arg(5),
  90. 'callback' => 'drupal_get_form',
  91. 'callback arguments' => array('apachesolr_mlt_delete_block_form', arg(5)),
  92. 'access' => user_access('administer search'),
  93. 'type' => MENU_CALLBACK,
  94. );
  95. }
  96. }
  97. return $items;
  98. }
  99. /**
  100. * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.)
  101. * Depending on the admin settings, possibly redirect to Drupal's core search.
  102. *
  103. * @param $search_name
  104. * The name of the search implementation.
  105. *
  106. * @param $querystring
  107. * The search query that was issued at the time of failure.
  108. */
  109. function apachesolr_failure($search_name, $querystring) {
  110. $fail_rule = variable_get('apachesolr_failure', 'show_error');
  111. switch ($fail_rule) {
  112. case 'show_error':
  113. drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error');
  114. break;
  115. case 'show_drupal_results':
  116. drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name)));
  117. drupal_goto('search/node/' . drupal_urlencode($querystring));
  118. break;
  119. case 'show_no_results':
  120. return;
  121. }
  122. }
  123. /**
  124. * Like $site_key in _update_refresh() - returns a site-specific hash.
  125. */
  126. function apachesolr_site_hash() {
  127. if (!($hash = variable_get('apachesolr_site_hash', FALSE))) {
  128. global $base_url;
  129. // Set a random 6 digit base-36 number as the hash.
  130. $hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6);
  131. variable_set('apachesolr_site_hash', $hash);
  132. }
  133. return $hash;
  134. }
  135. /**
  136. * Generate a unique ID for an entity being indexed.
  137. *
  138. * @param $id
  139. * An id number (or string) unique to this site, such as a node ID.
  140. * @param $entity
  141. * A string like 'node', 'file', 'user', or some other Drupal object type.
  142. *
  143. * @return
  144. * A string combining the parameters with the site hash.
  145. */
  146. function apachesolr_document_id($id, $entity = 'node') {
  147. return apachesolr_site_hash() . "/$entity/" . $id;
  148. }
  149. /**
  150. * Implementation of hook_user().
  151. *
  152. * Mark nodes as needing re-indexing if the author name changes.
  153. */
  154. function apachesolr_user($op, &$edit, &$account) {
  155. switch ($op) {
  156. case 'update':
  157. if (isset($edit['name']) && $account->name != $edit['name']) {
  158. switch ($GLOBALS['db_type']) {
  159. case 'mysql':
  160. case 'mysqli':
  161. db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.uid = %d", time(), $account->uid);
  162. break;
  163. default:
  164. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid);
  165. break;
  166. }
  167. }
  168. break;
  169. }
  170. }
  171. /**
  172. * Implementation of hook_taxonomy().
  173. *
  174. * Mark nodes as needing re-indexing if a term name changes.
  175. */
  176. function apachesolr_taxonomy($op, $type, $edit) {
  177. if ($type == 'term' && ($op == 'update')) {
  178. switch ($GLOBALS['db_type']) {
  179. case 'mysql':
  180. case 'mysqli':
  181. db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {term_node} tn ON asn.nid = tn.nid SET asn.changed = %d WHERE tn.tid = %d", time(), $edit['tid']);
  182. break;
  183. default:
  184. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']);
  185. break;
  186. }
  187. }
  188. // TODO: the rest, such as term deletion.
  189. }
  190. /**
  191. * Implementation of hook_comment().
  192. *
  193. * Mark nodes as needing re-indexing if comments are added or changed.
  194. * Like search_comment().
  195. */
  196. function apachesolr_comment($edit, $op) {
  197. $edit = (array) $edit;
  198. switch ($op) {
  199. // Reindex the node when comments are added or changed
  200. case 'insert':
  201. case 'update':
  202. case 'delete':
  203. case 'publish':
  204. case 'unpublish':
  205. // TODO: do we want to skip this if we are excluding comments
  206. // from the index for this node type?
  207. apachesolr_mark_node($edit['nid']);
  208. break;
  209. }
  210. }
  211. /**
  212. * Mark one node as needing re-indexing.
  213. */
  214. function apachesolr_mark_node($nid) {
  215. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid);
  216. }
  217. /**
  218. * Implementation of hook_node_type().
  219. *
  220. * Mark nodes as needing re-indexing if a node type name changes.
  221. */
  222. function apachesolr_node_type($op, $info) {
  223. if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) {
  224. // We cannot be sure we are going before or after node module.
  225. switch ($GLOBALS['db_type']) {
  226. case 'mysql':
  227. case 'mysqli':
  228. db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE (n.type = '%s' OR n.type = '%s')", time(), $info->old_type, $info->type);
  229. break;
  230. default:
  231. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s' OR type = '%s')", time(), $info->old_type, $info->type);
  232. break;
  233. }
  234. }
  235. }
  236. /**
  237. * Helper function for modules implementing hook_search's 'status' op.
  238. */
  239. function apachesolr_index_status($namespace) {
  240. list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
  241. $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE asn.status = 1 " . $exclude_sql, $excluded_types));
  242. $remaining = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 " . $exclude_sql, $args));
  243. return array('remaining' => $remaining, 'total' => $total);
  244. }
  245. /**
  246. * Returns last changed and last nid for an indexing namespace.
  247. */
  248. function apachesolr_get_last_index($namespace) {
  249. $stored = variable_get('apachesolr_index_last', array());
  250. return isset($stored[$namespace]) ? $stored[$namespace] : array('last_change' => 0, 'last_nid' => 0);
  251. }
  252. /**
  253. * Clear a specific namespace's last changed and nid, or clear all.
  254. */
  255. function apachesolr_clear_last_index($namespace = '') {
  256. if ($namespace) {
  257. $stored = variable_get('apachesolr_index_last', array());
  258. unset($stored[$namespace]);
  259. variable_set('apachesolr_index_last', $stored);
  260. }
  261. else {
  262. variable_del('apachesolr_index_last');
  263. }
  264. }
  265. /**
  266. * Truncate and rebuild the apachesolr_search_node table, reset the apachesolr_index_last variable.
  267. * This is the most complete way to force reindexing, or to build the indexing table for the
  268. * first time.
  269. *
  270. * @param $type
  271. * A single content type to be reindexed, leaving the others unaltered.
  272. */
  273. function apachesolr_rebuild_index_table($type = NULL) {
  274. if (isset($type)) {
  275. switch ($GLOBALS['db_type']) {
  276. case 'mysql':
  277. case 'mysqli':
  278. db_query("DELETE FROM {apachesolr_search_node} USING {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid WHERE n.type = '%s'", $type);
  279. break;
  280. default:
  281. db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type);
  282. break;
  283. }
  284. // Populate table
  285. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
  286. SELECT n.nid, n.status, %d AS changed
  287. FROM {node} n WHERE n.type = '%s'", time(), $type);
  288. }
  289. else {
  290. db_query("DELETE FROM {apachesolr_search_node}");
  291. // Populate table.
  292. if (module_exists('comment')) {
  293. // If comment module is enabled, use last_comment_timestamp as well.
  294. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
  295. SELECT n.nid, n.status, GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0)) AS changed
  296. FROM {node} n
  297. LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid");
  298. }
  299. else {
  300. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
  301. SELECT n.nid, n.status, GREATEST(n.created, n.changed) AS changed
  302. FROM {node} n");
  303. }
  304. // Make sure no nodes end up with a timestamp that's in the future.
  305. $time = time();
  306. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE changed > %d", $time, $time);
  307. apachesolr_clear_last_index();
  308. }
  309. }
  310. function _apachesolr_exclude_types($namespace) {
  311. extract(apachesolr_get_last_index($namespace));
  312. $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace);
  313. $args = array($last_change, $last_change, $last_nid);
  314. $join_sql = '';
  315. $exclude_sql = '';
  316. if ($excluded_types) {
  317. $excluded_types = array_unique($excluded_types);
  318. $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid ";
  319. $exclude_sql = "AND n.type NOT IN(". db_placeholders($excluded_types, 'varchar') .") ";
  320. $args = array_merge($args, $excluded_types);
  321. }
  322. return array($excluded_types, $args, $join_sql, $exclude_sql);
  323. }
  324. /**
  325. * Returns an array of rows from a query based on an indexing namespace.
  326. */
  327. function apachesolr_get_nodes_to_index($namespace, $limit) {
  328. $rows = array();
  329. if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE)) {
  330. return $rows;
  331. }
  332. list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace);
  333. $result = db_query_range("SELECT asn.nid, asn.changed FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 ". $exclude_sql ."ORDER BY asn.changed ASC, asn.nid ASC", $args, 0, $limit);
  334. while ($row = db_fetch_object($result)) {
  335. $rows[] = $row;
  336. }
  337. return $rows;
  338. }
  339. /**
  340. * Function to handle the indexing of nodes.
  341. *
  342. * The calling function must supply a name space or track/store
  343. * the timestamp and nid returned.
  344. * Returns FALSE if no nodes were indexed (none found or error).
  345. */
  346. function apachesolr_index_nodes($rows, $namespace = '', $callback = 'apachesolr_add_node_document') {
  347. if (!$rows) {
  348. // Nothing to do.
  349. return FALSE;
  350. }
  351. try {
  352. // Get the $solr object
  353. $solr = apachesolr_get_solr();
  354. // If there is no server available, don't continue.
  355. if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) {
  356. throw new Exception(t('No Solr instance available during indexing.'));
  357. }
  358. }
  359. catch (Exception $e) {
  360. watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), WATCHDOG_ERROR);
  361. return FALSE;
  362. }
  363. include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc');
  364. $documents = array();
  365. $old_position = apachesolr_get_last_index($namespace);
  366. $position = $old_position;
  367. // Always build the content for the index as an anonynmous user.
  368. global $user;
  369. session_save_session(FALSE);
  370. $saved_user = $user;
  371. $user = drupal_anonymous_user();
  372. foreach ($rows as $row) {
  373. try {
  374. $callback($documents, $row->nid, $namespace);
  375. // Variables to track the last item changed.
  376. $position['last_change'] = $row->changed;
  377. $position['last_nid'] = $row->nid;
  378. }
  379. catch (Exception $e) {
  380. // Something bad happened - don't continue.
  381. watchdog('Apache Solr', t('Error constructing documents to index: <br /> !message', array('!message' => nl2br(strip_tags($e->getMessage())))), WATCHDOG_ERROR);
  382. break;
  383. }
  384. }
  385. // Restore the user.
  386. $user = $saved_user;
  387. session_save_session(TRUE);
  388. if (count($documents)) {
  389. try {
  390. watchdog('Apache Solr', t('Adding @count documents.', array('@count' => count($documents))));
  391. // Chunk the adds by 20s
  392. $docs_chunk = array_chunk($documents, 20);
  393. foreach ($docs_chunk as $docs) {
  394. $solr->addDocuments($docs);
  395. }
  396. // Set the timestamp to indicate an index update.
  397. apachesolr_index_updated(time());
  398. }
  399. catch (Exception $e) {
  400. $nids = array();
  401. if (!empty($docs)) {
  402. foreach ($docs as $doc) {
  403. $nids[] = $doc->nid;
  404. }
  405. }
  406. watchdog('Apache Solr', t('Indexing failed on one of the following nodes: @nids <br /> !message', array('@nids' => implode(', ', $nids), '!message' => nl2br(strip_tags($e->getMessage())))), WATCHDOG_ERROR);
  407. return FALSE;
  408. }
  409. }
  410. // Save the new position in case it changed.
  411. if ($namespace && $position != $old_position) {
  412. $stored = variable_get('apachesolr_index_last', array());
  413. $stored[$namespace] = $position;
  414. variable_set('apachesolr_index_last', $stored);
  415. }
  416. return $position;
  417. }
  418. /**
  419. * Convert date from timestamp into ISO 8601 format.
  420. * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
  421. */
  422. function apachesolr_date_iso($date_timestamp) {
  423. return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp);
  424. }
  425. function apachesolr_delete_node_from_index($node) {
  426. static $failed = FALSE;
  427. if ($failed) {
  428. return FALSE;
  429. }
  430. try {
  431. $solr = apachesolr_get_solr();
  432. $solr->deleteById(apachesolr_document_id($node->nid));
  433. apachesolr_index_updated(time());
  434. return TRUE;
  435. }
  436. catch (Exception $e) {
  437. watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), WATCHDOG_ERROR);
  438. // Don't keep trying queries if they are failing.
  439. $failed = TRUE;
  440. return FALSE;
  441. }
  442. }
  443. /**
  444. * Helper function to keep track of when the index has been updated.
  445. */
  446. function apachesolr_index_updated($updated = NULL) {
  447. if (isset($updated)) {
  448. if ($updated) {
  449. variable_set('apachesolr_index_updated', (int) $updated);
  450. }
  451. else {
  452. variable_del('apachesolr_index_updated');
  453. }
  454. }
  455. return variable_get('apachesolr_index_updated', 0);
  456. }
  457. /**
  458. * Implementation of hook_cron().
  459. */
  460. function apachesolr_cron() {
  461. // Mass update and delete functions are in the include file.
  462. include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc');
  463. apachesolr_cron_check_node_table();
  464. try {
  465. $solr = apachesolr_get_solr();
  466. // Optimize the index (by default once a day).
  467. $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24);
  468. $last = variable_get('apachesolr_last_optimize', 0);
  469. $time = time();
  470. if ($optimize_interval && ($time - $last > $optimize_interval)) {
  471. $solr->optimize(FALSE, FALSE);
  472. variable_set('apachesolr_last_optimize', $time);
  473. apachesolr_index_updated($time);
  474. }
  475. // Only clear the cache if the index changed.
  476. // TODO: clear on some schedule if running multi-site.
  477. $updated = apachesolr_index_updated();
  478. if ($updated) {
  479. $solr->clearCache();
  480. // Re-populate the luke cache.
  481. $solr->getLuke();
  482. // TODO: an admin interface for setting this. Assume for now 5 minutes.
  483. if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) {
  484. // Clear the updated flag.
  485. apachesolr_index_updated(FALSE);
  486. }
  487. }
  488. }
  489. catch (Exception $e) {
  490. watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) .' in apachesolr_cron', WATCHDOG_ERROR);
  491. }
  492. }
  493. /**
  494. * Implementation of hook_nodeapi().
  495. */
  496. function apachesolr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  497. switch ($op) {
  498. case 'delete':
  499. _apachesolr_nodeapi_delete($node);
  500. break;
  501. case 'insert':
  502. // Make sure no node ends up with a timestamp that's in the future
  503. // by using time() rather than the node's changed or created timestamp.
  504. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid, $node->status, time());
  505. break;
  506. case 'update':
  507. _apachesolr_nodeapi_update($node);
  508. break;
  509. }
  510. }
  511. /**
  512. * Helper function for hook_nodeapi().
  513. */
  514. function _apachesolr_nodeapi_delete($node, $set_message = TRUE) {
  515. if (apachesolr_delete_node_from_index($node)) {
  516. // There was no exception, so delete from the table.
  517. db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid);
  518. if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
  519. apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.');
  520. }
  521. }
  522. }
  523. /**
  524. * Helper function for hook_nodeapi().
  525. */
  526. function _apachesolr_nodeapi_update($node, $set_message = TRUE) {
  527. // Check if the node has gone from published to unpublished.
  528. if (!$node->status && db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid))) {
  529. if (apachesolr_delete_node_from_index($node)) {
  530. // There was no exception, so update the table.
  531. db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
  532. if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
  533. apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.');
  534. }
  535. }
  536. }
  537. else {
  538. db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
  539. }
  540. }
  541. /**
  542. * Call drupal_set_message() with the text.
  543. *
  544. * The text is translated with t() and substituted using Solr stats.
  545. */
  546. function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE) {
  547. try {
  548. $solr = apachesolr_get_solr();
  549. $stats_summary = $solr->getStatsSummary();
  550. drupal_set_message(t($text, $stats_summary), $type, FALSE);
  551. }
  552. catch (Exception $e) {
  553. watchdog('apachesolr', nl2br(check_plain($e->getMessage())), WATCHDOG_ERROR);
  554. }
  555. }
  556. /**
  557. * Return the enabled facets from the specified block array.
  558. *
  559. * @param $module
  560. * The module (optional).
  561. * @return
  562. * An array consisting of info for facets that have been enabled
  563. * for the specified module, or all enabled facets.
  564. */
  565. function apachesolr_get_enabled_facets($module = NULL) {
  566. $enabled = variable_get('apachesolr_enabled_facets', array());
  567. if (isset($module)) {
  568. return isset($enabled[$module]) ? $enabled[$module] : array();
  569. }
  570. return $enabled;
  571. }
  572. /**
  573. * Save the enabled facets for all modules.
  574. *
  575. * @param $enabled
  576. * An array consisting of info for all enabled facets.
  577. * @return
  578. * The array consisting of info for all enabled facets.
  579. */
  580. function apachesolr_save_enabled_facets($enabled) {
  581. variable_set('apachesolr_enabled_facets', $enabled);
  582. return $enabled;
  583. }
  584. /**
  585. * Save the enabled facets for one module.
  586. *
  587. * @param $module
  588. * The module name.
  589. * @param $facets
  590. * Associative array of $delta => $facet_field pairs. If omitted, all facets
  591. * for $module are disabled.
  592. * @return
  593. * An array consisting of info for all enabled facets.
  594. */
  595. function apachesolr_save_module_facets($module, $facets = array()) {
  596. $enabled = variable_get('apachesolr_enabled_facets', array());
  597. if (!empty($facets) && is_array($facets)) {
  598. $enabled[$module] = $facets;
  599. }
  600. else {
  601. unset($enabled[$module]);
  602. }
  603. variable_set('apachesolr_enabled_facets', $enabled);
  604. return $enabled;
  605. }
  606. /**
  607. * Implementation of hook_block().
  608. */
  609. function apachesolr_block($op = 'list', $delta = 0, $edit = array()) {
  610. static $access;
  611. switch ($op) {
  612. case 'list':
  613. // Get all of the moreLikeThis blocks that the user has created
  614. $blocks = apachesolr_mlt_list_blocks();
  615. // Add the sort block.
  616. $blocks['sort'] = array(
  617. 'info' => t('Apache Solr Core: Sorting'),
  618. /* From D6: 'cache' => BLOCK_CACHE_PER_PAGE, */
  619. );
  620. return $blocks;
  621. case 'view':
  622. // From D6: if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) {
  623. if ($delta != 'sort' && (arg(0) == 'node'&& is_numeric(arg(1)) && $node = node_load(arg(1))) && (!arg(2) || arg(2) == 'view')) {
  624. $suggestions = array();
  625. // Determine whether the user can view the current node.
  626. if (!isset($access)) {
  627. $access = node_access('view', $node);
  628. }
  629. $block = apachesolr_mlt_load_block($delta);
  630. if ($access && $block) {
  631. $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid));
  632. if (!empty($docs)) {
  633. $suggestions['subject'] = check_plain($block['name']);
  634. $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs);
  635. if (user_access('administer search')) {
  636. $suggestions['content'] .= l(t('Configure this block'), 'admin/build/block/configure/apachesolr/' . $delta, array('class' => 'apachesolr-mlt-admin-link'));
  637. }
  638. }
  639. }
  640. return $suggestions;
  641. }
  642. elseif (apachesolr_has_searched() && $delta == 'sort') {
  643. // Get the query and response. Without these no blocks make sense.
  644. $response = apachesolr_static_response_cache();
  645. if (empty($response) || ($response->response->numFound < 2)) {
  646. return;
  647. }
  648. $query = apachesolr_current_query();
  649. $sorts = $query->get_available_sorts();
  650. // Get the current sort as an array.
  651. $solrsort = $query->get_solrsort();
  652. $sort_links = array();
  653. $path = $query->get_path();
  654. $new_query = clone $query;
  655. $toggle = array('asc' => 'desc', 'desc' => 'asc');
  656. foreach ($sorts as $name => $sort) {
  657. $active = $solrsort['#name'] == $name;
  658. $direction = '';
  659. $new_direction = $sort['default'];
  660. if ($name == 'score') {
  661. // We only sort by ascending score.
  662. $new_direction = 'asc';
  663. }
  664. elseif ($active) {
  665. $direction = $toggle[$solrsort['#direction']];
  666. $new_direction = $toggle[$solrsort['#direction']];
  667. }
  668. $new_query->set_solrsort($name, $new_direction);
  669. $sort_links[$name] = array(
  670. 'title' => $sort['title'],
  671. 'path' => $path,
  672. 'options' => array('query' => $new_query->get_url_queryvalues()),
  673. 'active' => $active,
  674. 'direction' => $direction,
  675. );
  676. }
  677. // Allow other modules to add or remove sorts.
  678. drupal_alter('apachesolr_sort_links', $sort_links);
  679. if (!empty($sort_links)) {
  680. foreach ($sort_links as $name => $link) {
  681. $themed_links[$name] = theme('apachesolr_sort_link', $link['title'], $link['path'], $link['options'], $link['active'], $link['direction']);
  682. }
  683. return array(
  684. 'subject' => t('Sort by'),
  685. 'content' => theme('apachesolr_sort_list', $themed_links),
  686. );
  687. }
  688. }
  689. break;
  690. case 'configure':
  691. if ($delta != 'sort') {
  692. require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
  693. return apachesolr_mlt_block_form($delta);
  694. }
  695. break;
  696. case 'save':
  697. if ($delta != 'sort') {
  698. require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
  699. apachesolr_mlt_save_block($edit, $delta);
  700. }
  701. break;
  702. }
  703. }
  704. /**
  705. * Helper function for displaying a facet block.
  706. */
  707. function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
  708. if (!empty($response->facet_counts->facet_fields->$facet_field)) {
  709. $contains_active = FALSE;
  710. $items = array();
  711. foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) {
  712. $sortpre = 1000000 - $count;
  713. $options = array('delta' => $delta);
  714. $exclude = FALSE;
  715. // Solr sends this back if it's empty.
  716. if ($facet == '_empty_') {
  717. $exclude = TRUE;
  718. $facet = '[* TO *]';
  719. $facet_text = theme('placeholder', t('Missing this field'));
  720. $options['html'] = TRUE;
  721. // Put this just below any active facets.
  722. // '-' sorts before all numbers, but after '*'.
  723. $sortpre = '-';
  724. }
  725. else {
  726. $facet_text = $facet;
  727. }
  728. if ($facet_callback && function_exists($facet_callback)) {
  729. $facet_text = $facet_callback($facet, $options);
  730. }
  731. $unclick_link = '';
  732. $active = FALSE;
  733. $new_query = clone $query;
  734. if ($query->has_filter($facet_field, $facet)) {
  735. $contains_active = $active = TRUE;
  736. // '*' sorts before all numbers.
  737. $sortpre = '*';
  738. $new_query->remove_filter($facet_field, $facet);
  739. $options['query'] = $new_query->get_url_queryvalues();
  740. $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options);
  741. }
  742. else {
  743. $new_query->add_filter($facet_field, $facet, $exclude);
  744. $options['query'] = $new_query->get_url_queryvalues();
  745. $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound);
  746. }
  747. if ($count || $active) {
  748. $items[$sortpre . '*' . $facet_text] = $link;
  749. }
  750. }
  751. // Unless a facet is active only display 2 or more.
  752. if ($items && ($response->response->numFound > 1 || $contains_active)) {
  753. ksort($items, SORT_STRING);
  754. // Get information needed by the rest of the blocks about limits.
  755. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
  756. $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
  757. $output = theme('apachesolr_facet_list', $items, $limit);
  758. return array('subject' => $filter_by, 'content' => $output);
  759. }
  760. }
  761. return NULL;
  762. }
  763. /**
  764. * Helper function for displaying a date facet block.
  765. *
  766. * TODO: Refactor with apachesolr_facet_block().
  767. */
  768. function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
  769. $items = array();
  770. $new_query = clone $query;
  771. foreach (array_reverse($new_query->get_filters($facet_field)) as $filter) {
  772. $options = array();
  773. // Iteratively remove the date facets.
  774. $new_query->remove_filter($facet_field, $filter['#value']);
  775. if ($facet_callback && function_exists($facet_callback)) {
  776. $facet_text = $facet_callback($filter['#start'], $options);
  777. }
  778. else {
  779. $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']);
  780. }
  781. $options['query'] = $new_query->get_url_queryvalues();
  782. array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options));
  783. }
  784. // Add links for additional date filters.
  785. if (!empty($response->facet_counts->facet_dates->$facet_field)) {
  786. $field = clone $response->facet_counts->facet_dates->$facet_field;
  787. $end = $field->end;
  788. unset($field->end);
  789. $gap = $field->gap;
  790. unset($field->gap);
  791. // Treat each date facet as a range start, and use the next date
  792. // facet as range end. Use 'end' for the final end.
  793. $range_end = array();
  794. foreach ($field as $facet => $count) {
  795. if (isset($prev_facet)) {
  796. $range_end[$prev_facet] = $facet;
  797. }
  798. $prev_facet = $facet;
  799. }
  800. $range_end[$prev_facet] = $end;
  801. foreach ($field as $facet => $count) {
  802. $options = array();
  803. // Solr sends this back if it's empty.
  804. if ($facet == '_empty_' || $count == 0) {
  805. continue;
  806. }
  807. if ($facet_callback && function_exists($facet_callback)) {
  808. $facet_text = $facet_callback($facet, $options);
  809. }
  810. else {
  811. $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet);
  812. }
  813. $new_query = clone $query;
  814. $new_query->add_filter($facet_field, '['. $facet .' TO '. $range_end[$facet] .']');
  815. $options['query'] = $new_query->get_url_queryvalues();
  816. $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound);
  817. }
  818. }
  819. if (count($items) > 0) {
  820. // Get information needed by the rest of the blocks about limits.
  821. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
  822. $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
  823. $output = theme('apachesolr_facet_list', $items, $limit);
  824. return array('subject' => $filter_by, 'content' => $output);
  825. }
  826. return NULL;
  827. }
  828. /**
  829. * Determine the gap in a date range query filter that we generated.
  830. *
  831. * This function assumes that the start and end dates are the
  832. * beginning and end of a single period: 1 year, month, day, hour,
  833. * minute, or second (all date range query filters we generate meet
  834. * this criteria). So, if the seconds are different, it is a second
  835. * gap. If the seconds are the same (incidentally, they will also be
  836. * 0) but the minutes are different, it is a minute gap. If the
  837. * minutes are the same but hours are different, it's an hour gap.
  838. * etc.
  839. *
  840. * @param $start
  841. * Start date as an ISO date string.
  842. * @param $end
  843. * End date as an ISO date string.
  844. * @return
  845. * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND.
  846. */
  847. function apachesolr_date_find_query_gap($start_iso, $end_iso) {
  848. $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1);
  849. $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@';
  850. if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) {
  851. foreach ($gaps as $gap => $idx) {
  852. if ($start[$idx] != $end[$idx]) {
  853. return $gap;
  854. }
  855. }
  856. }
  857. // can't tell
  858. return 'YEAR';
  859. }
  860. /**
  861. * Format an ISO date string based on the gap used to generate it.
  862. *
  863. * This function assumes that gaps less than one day will be displayed
  864. * in a search context in which a larger containing gap including a
  865. * day is already displayed. So, HOUR, MINUTE, and SECOND gaps only
  866. * display time information, without date.
  867. *
  868. * @param $gap
  869. * A gap.
  870. * @param $iso
  871. * An ISO date string.
  872. * @return
  873. * A gap-appropriate formatted date.
  874. */
  875. function apachesolr_date_format_iso_by_gap($gap, $iso) {
  876. // TODO: If we assume that multiple search queries are formatted in
  877. // order, we could store a static list of all gaps we've formatted.
  878. // Then, if we format an HOUR, MINUTE, or SECOND without previously
  879. // having formatted a DAY or later, we could include date
  880. // information. However, we'd need to do that per-field and I'm not
  881. // our callers always have field information handy.
  882. $unix = strtotime($iso);
  883. if ($unix !== FALSE) {
  884. switch ($gap) {
  885. case 'YEAR':
  886. return format_date($unix, 'custom', 'Y', 0);
  887. case 'MONTH':
  888. return format_date($unix, 'custom', 'F Y', 0);
  889. case 'DAY':
  890. return format_date($unix, 'custom', 'F j, Y', 0);
  891. case 'HOUR':
  892. return format_date($unix, 'custom', 'g A', 0);
  893. case 'MINUTE':
  894. return format_date($unix, 'custom', 'g:i A', 0);
  895. case 'SECOND':
  896. return format_date($unix, 'custom', 'g:i:s A', 0);
  897. }
  898. }
  899. return $iso;
  900. }
  901. /**
  902. * Format the beginning of a date range query filter that we
  903. * generated.
  904. *
  905. * @param $start_iso
  906. * The start date.
  907. * @param $end_iso
  908. * The end date.
  909. * @return
  910. * A display string reprepsenting the date range, such as "January
  911. * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z"
  912. */
  913. function apachesolr_date_format_range($start_iso, $end_iso) {
  914. $gap = apachesolr_date_find_query_gap($start_iso, $end_iso);
  915. return apachesolr_date_format_iso_by_gap($gap, $start_iso);
  916. }
  917. /**
  918. * Determine the best search gap to use for an arbitrary date range.
  919. *
  920. * Generally, we the maximum gap that fits between the start and end
  921. * date. If they are more than a year apart, 1 year; if they are more
  922. * than a month apart, 1 month; etc.
  923. *
  924. * This function uses Unix timestamps for its computation and so is
  925. * not useful for dates outside that range.
  926. *
  927. * @param $start
  928. * Start date as an ISO date string.
  929. * @param $end
  930. * End date as an ISO date string.
  931. * @return
  932. * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far
  933. * apart $start and $end are.
  934. */
  935. function apachesolr_date_determine_gap($start, $end) {
  936. $start = strtotime($start);
  937. $end = strtotime($end);
  938. if ($end - $start >= 86400*365) {
  939. return 'YEAR';
  940. }
  941. if (date('Ym', $start) != date('Ym', $end)) {
  942. return 'MONTH';
  943. }
  944. if ($end - $start > 86400) {
  945. return 'DAY';
  946. }
  947. // For now, HOUR is a reasonable smallest gap.
  948. return 'HOUR';
  949. }
  950. /**
  951. * Return the next smaller date gap.
  952. *
  953. * @param $gap
  954. * A gap.
  955. * @return
  956. * The next smaller gap, or NULL if there is no smaller gap.
  957. */
  958. function apachesolr_date_gap_drilldown($gap) {
  959. $drill = array(
  960. 'YEAR' => 'MONTH',
  961. 'MONTH' => 'DAY',
  962. 'DAY' => 'HOUR',
  963. // For now, HOUR is a reasonable smallest gap.
  964. // 'HOUR' => 'MINUTE',
  965. );
  966. return isset($drill[$gap]) ? $drill[$gap] : NULL;
  967. }
  968. /**
  969. * Used by the 'configure' $op of hook_block so that modules can generically set
  970. * facet limits on their blocks.
  971. */
  972. function apachesolr_facetcount_form($module, $delta) {
  973. $initial = variable_get('apachesolr_facet_query_initial_limits', array());
  974. $limits = variable_get('apachesolr_facet_query_limits', array());
  975. $facet_missing = variable_get('apachesolr_facet_missing', array());
  976. $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3));
  977. $form['apachesolr_facet_query_initial_limit'] = array(
  978. '#type' => 'select',
  979. '#title' => t('Initial filter links'),
  980. '#options' => $limit,
  981. '#description' => t('The initial number of filter links to show in this block.'),
  982. '#default_value' => isset($initial[$module][$delta]) ? $initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10),
  983. );
  984. $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3));
  985. $form['apachesolr_facet_query_limit'] = array(
  986. '#type' => 'select',
  987. '#title' => t('Maximum filter links'),
  988. '#options' => $limit,
  989. '#description' => t('The maximum number of filter links to show in this block.'),
  990. '#default_value' => isset($limits[$module][$delta]) ? $limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20),
  991. );
  992. $form['apachesolr_facet_missing'] = array(
  993. '#type' => 'radios',
  994. '#title' => t('Include a facet for missing'),
  995. '#options' => array(0 => t('No'), 1 => t('Yes')),
  996. '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'),
  997. '#default_value' => isset($facet_missing[$module][$delta]) ? $facet_missing[$module][$delta] : 0,
  998. );
  999. return $form;
  1000. }
  1001. /**
  1002. * Used by the 'save' $op of hook_block so that modules can generically set
  1003. * facet limits on their blocks.
  1004. */
  1005. function apachesolr_facetcount_save($edit) {
  1006. // Save query limits
  1007. $module = $edit['module'];
  1008. $delta = $edit['delta'];
  1009. $limits = variable_get('apachesolr_facet_query_limits', array());
  1010. $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit'];
  1011. variable_set('apachesolr_facet_query_limits', $limits);
  1012. $initial = variable_get('apachesolr_facet_query_initial_limits', array());
  1013. $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit'];
  1014. variable_set('apachesolr_facet_query_initial_limits', $initial);
  1015. $facet_missing = variable_get('apachesolr_facet_missing', array());
  1016. $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing'];
  1017. variable_set('apachesolr_facet_missing', $facet_missing);
  1018. }
  1019. /**
  1020. * Initialize a pager for theme('pager') without running an SQL query.
  1021. *
  1022. * @see pager_query()
  1023. *
  1024. * @param $total
  1025. * The total number of items found.
  1026. * @param $limit
  1027. * The number of items you will display per page.
  1028. * @param $element
  1029. * An optional integer to distinguish between multiple pagers on one page.
  1030. *
  1031. * @return
  1032. * The current page for $element. 0 by default if $_GET['page'] is empty.
  1033. */
  1034. function apachesolr_pager_init($total, $limit = 10, $element = 0) {
  1035. global $pager_page_array, $pager_total, $pager_total_items;
  1036. $page = isset($_GET['page']) ? $_GET['page'] : '';
  1037. // Convert comma-separated $page to an array, used by other functions.
  1038. $pager_page_array = explode(',', $page);
  1039. // We calculate the total of pages as ceil(items / limit).
  1040. $pager_total_items[$element] = $total;
  1041. $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
  1042. $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1));
  1043. return $pager_page_array[$element];
  1044. }
  1045. /**
  1046. * This hook allows modules to modify the query and params objects.
  1047. *
  1048. * Example:
  1049. *
  1050. * function my_module_apachesolr_modify_query(&$query, &$params) {
  1051. * // I only want to see articles by the admin!
  1052. * $query->add_filter("uid", 1);
  1053. *
  1054. * }
  1055. */
  1056. function apachesolr_modify_query(&$query, &$params, $caller) {
  1057. if (empty($query)) {
  1058. // This should only happen if Solr is not set up - avoids fatal errors.
  1059. return;
  1060. }
  1061. foreach (module_implements('apachesolr_modify_query') as $module) {
  1062. $function_name = $module . '_apachesolr_modify_query';
  1063. $function_name($query, $params, $caller);
  1064. }
  1065. // TODO: The query object should hold all the params.
  1066. // Add array of fq parameters.
  1067. if ($query && ($fq = $query->get_fq())) {
  1068. $params['fq'] = $fq;
  1069. }
  1070. // Add sort if present.
  1071. if ($query) {
  1072. $sort = $query->get_solrsort();
  1073. $sortstring = $sort['#name'] .' '. $sort['#direction'];
  1074. // We don't bother telling Solr to do its default sort.
  1075. if ($sortstring != 'score asc') {
  1076. $params['sort'] = $sortstring;
  1077. }
  1078. }
  1079. }
  1080. /**
  1081. * Semaphore that indicates whether a search has been done. Blocks use this
  1082. * later to decide whether they should load or not.
  1083. *
  1084. * @param $searched
  1085. * A boolean indicating whether a search has been executed.
  1086. *
  1087. * @return
  1088. * TRUE if a search has been executed.
  1089. * FALSE otherwise.
  1090. */
  1091. function apachesolr_has_searched($searched = NULL) {
  1092. static $_searched = FALSE;
  1093. if (is_bool($searched)) {
  1094. $_searched = $searched;
  1095. }
  1096. return $_searched;
  1097. }
  1098. /**
  1099. * Factory method for solr singleton object. Structure allows for an arbitrary
  1100. * number of solr objects to be used based on the host, port, path combination.
  1101. * Get an instance like this:
  1102. * $solr = apachesolr_get_solr();
  1103. */
  1104. function apachesolr_get_solr($host = NULL, $port = NULL, $path = NULL) {
  1105. static $solr_cache;
  1106. if (empty($host)) {
  1107. $host = variable_get('apachesolr_host', 'localhost');
  1108. }
  1109. if (empty($port)) {
  1110. $port = variable_get('apachesolr_port', '8983');
  1111. }
  1112. if (empty($path)) {
  1113. $path = variable_get('apachesolr_path', '/solr');
  1114. }
  1115. if (empty($solr_cache[$host][$port][$path])) {
  1116. list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service'));
  1117. include_once(drupal_get_path('module', $module) .'/'. $filepath);
  1118. $solr = new $class($host, $port, $path);
  1119. // Set a non-default behavior.
  1120. $solr->setCollapseSingleValueArrays(FALSE);
  1121. $solr_cache[$host][$port][$path] = $solr;
  1122. }
  1123. return $solr_cache[$host][$port][$path];
  1124. }
  1125. /**
  1126. * Execute a search based on a query object.
  1127. *
  1128. * Normally this function is used with the default (dismax) handler for keyword
  1129. * searches. The $final_query that's returned will have been modified by
  1130. * both hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().
  1131. *
  1132. * @param $caller
  1133. * String, name of the calling module or function for use as a cache namespace.
  1134. * @param $current_query
  1135. * A query object from apachesolr_drupal_query(). It will be modified by
  1136. * hook_apachesolr_prepare_query() and then cached in apachesolr_current_query().
  1137. * @param $params
  1138. * Array of parameters to pass to Solr. Must include at least 'rows'.
  1139. * @param $page
  1140. * For paging into results, using $params['rows'] results per page.
  1141. *
  1142. * @return array($final_query, $response)
  1143. *
  1144. * @throws Exception
  1145. */
  1146. function apachesolr_do_query($caller, $current_query, &$params = array('rows' => 10), $page = 0) {
  1147. // Allow modules to alter the query prior to statically caching it.
  1148. // This can e.g. be used to add available sorts.
  1149. foreach (module_implements('apachesolr_prepare_query') as $module) {
  1150. $function_name = $module . '_apachesolr_prepare_query';
  1151. $function_name($current_query, $params, $caller);
  1152. }
  1153. // Cache the original query. Since all the built queries go through
  1154. // this process, all the hook_invocations will happen later
  1155. $query = apachesolr_current_query($current_query, $caller);
  1156. // This hook allows modules to modify the query and params objects.
  1157. apachesolr_modify_query($query, $params, $caller);
  1158. $params['start'] = $page * $params['rows'];
  1159. if (!$query) {
  1160. return array(NULL, array());
  1161. }
  1162. // Final chance for the caller to modify the query and params. The signature
  1163. // is: CALLER_finalize_query(&$query, &$params);
  1164. $function = $caller . '_finalize_query';
  1165. if (function_exists($function)) {
  1166. $function($query, $params);
  1167. }
  1168. $keys = $query->get_query_basic();
  1169. if ($keys == '' && isset($params['fq'])) {
  1170. // Move the fq params to q.alt for better performance.
  1171. $qalt = array();
  1172. foreach ($params['fq'] as $delta => $value) {
  1173. // Move the fq param if it has no local params and is not negative.
  1174. if (!preg_match('/^(?:\{!|-)/', $value)) {
  1175. $qalt[] = $value;
  1176. unset($params['fq'][$delta]);
  1177. }
  1178. }
  1179. if ($qalt) {
  1180. $params['q.alt'] = implode(' ', $qalt);
  1181. }
  1182. }
  1183. // This is the object that does the communication with the solr server.
  1184. $solr = apachesolr_get_solr();
  1185. // We must run htmlspecialchars() here since converted entities are in the index.
  1186. // and thus bare entities &, > or < won't match.
  1187. $response = $solr->search(htmlspecialchars($keys, ENT_NOQUOTES, 'UTF-8'), $params['start'], $params['rows'], $params);
  1188. // The response is cached so that it is accessible to the blocks and anything
  1189. // else that needs it beyond the initial search.
  1190. apachesolr_static_response_cache($response, $caller);
  1191. return array($query, $response);
  1192. }
  1193. /**
  1194. * It is important to hold on to the Solr response object for the duration of the
  1195. * page request so that we can use it for things like building facet blocks.
  1196. *
  1197. * @todo reverse the order of parameters in future branches.
  1198. */
  1199. function apachesolr_static_response_cache($response = NULL, $namespace = 'apachesolr_search') {
  1200. static $_response = array();
  1201. if (is_object($response)) {
  1202. $_response[$namespace] = clone $response;
  1203. }
  1204. if (!isset($_response[$namespace])) {
  1205. $_response[$namespace] = NULL;
  1206. }
  1207. return $_response[$namespace];
  1208. }
  1209. /**
  1210. * Factory function for query objects.
  1211. *
  1212. * @param $keys
  1213. * The string that a user would type into the search box. Suitable input
  1214. * may come from search_get_keys().
  1215. *
  1216. * @param $filters
  1217. * Key and value pairs that are applied as a filter query.
  1218. *
  1219. * @param $solrsort
  1220. * Visible string telling solr how to sort.
  1221. *
  1222. * @param $base_path
  1223. * The search base path (without the keywords) for this query.
  1224. */
  1225. function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '') {
  1226. list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query'));
  1227. include_once drupal_get_path('module', $module) .'/'. $class .'.php';
  1228. try {
  1229. $query = new $class(apachesolr_get_solr(), $keys, $filters, $solrsort, $base_path);
  1230. }
  1231. catch (Exception $e) {
  1232. watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), WATCHDOG_ERROR);
  1233. $query = NULL;
  1234. }
  1235. return $query;
  1236. }
  1237. /**
  1238. * Static getter/setter for the current query
  1239. *
  1240. * @todo reverse the order of parameters in future branches.
  1241. */
  1242. function apachesolr_current_query($query = NULL, $namespace = 'apachesolr_search') {
  1243. static $saved_query = array();
  1244. if (is_object($query)) {
  1245. $saved_query[$namespace] = clone $query;
  1246. }
  1247. return is_object($saved_query[$namespace]) ? clone $saved_query[$namespace] : NULL;
  1248. }
  1249. /**
  1250. * array('index_type' => 'integer',
  1251. * 'multiple' => TRUE,
  1252. * 'name' => 'fieldname',
  1253. * ),
  1254. */
  1255. function apachesolr_index_key($field) {
  1256. switch ($field['index_type']) {
  1257. case 'text':
  1258. $type_prefix = 't';
  1259. break;
  1260. case 'string':
  1261. $type_prefix = 's';
  1262. break;
  1263. case 'integer':
  1264. $type_prefix = 'i';
  1265. break;
  1266. case 'sint':
  1267. $type_prefix = 'si';
  1268. break;
  1269. case 'double':
  1270. $type_prefix = 'p'; // reserve d for date
  1271. break;
  1272. case 'boolean':
  1273. $type_prefix = 'b';
  1274. break;
  1275. case 'date':
  1276. $type_prefix = 'd';
  1277. break;
  1278. case 'float':
  1279. $type_prefix = 'f';
  1280. break;
  1281. case 'tdate':
  1282. $type_prefix = 'td';
  1283. break;
  1284. case 'tint':
  1285. $type_prefix = 'ti';
  1286. break;
  1287. case 'tlong';
  1288. $type_prefix = 'tl';
  1289. break;
  1290. case 'tfloat':
  1291. $type_prefix = 'tf';
  1292. break;
  1293. case 'tdouble':
  1294. $type_prefix = 'tp';
  1295. break;
  1296. default:
  1297. $type_prefix = 's';
  1298. }
  1299. $sm = $field['multiple'] ? 'm_' : 's_';
  1300. return $type_prefix . $sm . $field['name'];
  1301. }
  1302. /**
  1303. * Try to map a schema field name to a human-readable description.
  1304. */
  1305. function apachesolr_field_name_map($field_name) {
  1306. require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
  1307. return _apachesolr_field_name_map($field_name);
  1308. }
  1309. /**
  1310. * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
  1311. */
  1312. function apachesolr_cck_fields($field = NULL, $reset = FALSE) {
  1313. static $fields;
  1314. if (!isset($fields) || $reset) {
  1315. if (!$reset && ($cache = cache_get('cck_fields', 'cache_apachesolr'))) {
  1316. $fields = unserialize($cache->data);
  1317. }
  1318. else {
  1319. $fields = array();
  1320. // If CCK isn't enabled, do nothing.
  1321. if (module_exists('content')) {
  1322. // A single default mapping for all text fields.
  1323. $mappings['text'] = array(
  1324. 'options_select' => array(
  1325. 'indexing_callback' => '',
  1326. 'display_callback' => 'apachesolr_cck_text_field_callback',
  1327. 'index_type' => 'string',
  1328. ),
  1329. 'options_buttons' => array(
  1330. 'indexing_callback' => '',
  1331. 'display_callback' => 'apachesolr_cck_text_field_callback',
  1332. 'index_type' => 'string',
  1333. ),
  1334. );
  1335. $mappings['nodereference'] = array(
  1336. 'nodereference_select' => array(
  1337. 'display_callback' => 'apachesolr_cck_nodereference_field_callback',
  1338. 'index_type' => 'integer',
  1339. ),
  1340. 'nodereference_autocomplete' => array(
  1341. 'display_callback' => 'apachesolr_cck_nodereference_field_callback',
  1342. 'index_type' => 'integer',
  1343. ),
  1344. );
  1345. $mappings['userreference'] = array(
  1346. 'userreference_select' => array(
  1347. 'display_callback' => 'apachesolr_cck_userreference_field_callback',
  1348. 'index_type' => 'integer',
  1349. ),
  1350. 'userreference_autocomplete' => array(
  1351. 'display_callback' => 'apachesolr_cck_userreference_field_callback',
  1352. 'index_type' => 'integer',
  1353. ),
  1354. );
  1355. // A field-specific mapping would look like:
  1356. // $mappings['per-field']['field_model_name'] = array('callback' => '', 'index_type' => 'string');
  1357. // or
  1358. // $mappings['per-field']['field_model_price'] = array('callback' => '', 'index_type' => 'float');
  1359. // Allow other modules to add or alter mappings.
  1360. drupal_alter('apachesolr_cck_fields', $mappings);
  1361. $result = db_query("SELECT i.field_name, f.multiple, f.type AS field_type, i.widget_type, i.label, i.type_name AS content_type FROM {node_field_instance} i INNER JOIN {node_field} f ON i.field_name = f.field_name;");
  1362. while ($row = db_fetch_object($result)) {
  1363. // Only deal with fields that have option widgets (facets
  1364. // don't make sense otherwise), or fields that have specific mappings.
  1365. if ((isset($mappings[$row->field_type][$row->widget_type]) || isset($mappings['per-field'][$row->field_name]))) {
  1366. if (isset($mappings['per-field'][$row->field_name])) {
  1367. $row->index_type = $mappings['per-field'][$row->field_name]['index_type'];
  1368. $row->indexing_callback = $mappings['per-field'][$row->field_name]['indexing_callback'];
  1369. $row->display_callback = $mappings['per-field'][$row->field_name]['display_callback'];
  1370. }
  1371. else {
  1372. $row->index_type = $mappings[$row->field_type][$row->widget_type]['index_type'];
  1373. $row->indexing_callback = $mappings[$row->field_type][$row->widget_type]['indexing_callback'];
  1374. $row->display_callback = $mappings[$row->field_type][$row->widget_type]['display_callback'];
  1375. }
  1376. $row->multiple = (bool) $row->multiple;
  1377. // It's important that we put the 'cck_' here because several points in the later processing
  1378. // depend on it to route program flow to cck specific handlers.
  1379. $row->name = 'cck_' . $row->field_name;
  1380. $fields[$row->field_name] = array_merge((array) $fields[$row->field_name], (array) $row);
  1381. $fields[$row->field_name]['content types'][] = $row->content_type;
  1382. unset($fields[$row->field_name]['content_type']);
  1383. }
  1384. }
  1385. }
  1386. cache_set('cck_fields', 'cache_apachesolr', serialize($fields));
  1387. }
  1388. }
  1389. return is_null($field) ? $fields : $fields[$field];
  1390. }
  1391. /**
  1392. * Use the content.module's content_format() to format the
  1393. * field based on its value ($facet).
  1394. *
  1395. * @param $facet string
  1396. * The indexed value
  1397. * @param $options
  1398. * An array of options including the hook_block $delta.
  1399. */
  1400. function apachesolr_cck_text_field_callback($facet, $options) {
  1401. if (function_exists('content_format')) {
  1402. return content_format($options['delta'], array('value' => $facet));
  1403. }
  1404. else {
  1405. return $facet;
  1406. }
  1407. }
  1408. /**
  1409. * Use the content.module's content_format() to format the
  1410. * field based on its nid ($facet).
  1411. *
  1412. * @param $facet string
  1413. * The indexed value
  1414. * @param $options
  1415. * An array of options including the hook_block $delta.
  1416. */
  1417. function apachesolr_cck_nodereference_field_callback($facet, $options) {
  1418. if (function_exists('content_format')) {
  1419. return content_format($options['delta'], array('nid' => $facet), 'plain');
  1420. }
  1421. else {
  1422. return $facet;
  1423. }
  1424. }
  1425. /**
  1426. * Use the content.module's content_format() to format the
  1427. * field based on its uid ($facet).
  1428. *
  1429. * @param $facet string
  1430. * The indexed value
  1431. * @param $options
  1432. * An array of options including the hook_block $delta.
  1433. */
  1434. function apachesolr_cck_userreference_field_callback($facet, $options) {
  1435. if (function_exists('content_format')) {
  1436. return content_format($options['delta'], array('uid' => $facet), 'plain');
  1437. }
  1438. else {
  1439. return $facet;
  1440. }
  1441. }
  1442. /**
  1443. * Performs a moreLikeThis query using the settings and retrieves documents.
  1444. *
  1445. * @param $settings
  1446. * An array of settings.
  1447. * @param $id
  1448. * The Solr ID of the document for which you want related content.
  1449. * For a node that is apachesolr_document_id($node->nid)
  1450. *
  1451. * @return An array of response documents, or NULL
  1452. */
  1453. function apachesolr_mlt_suggestions($settings, $id) {
  1454. try {
  1455. $solr = apachesolr_get_solr();
  1456. $fields = array(
  1457. 'mlt_mintf' => 'mlt.mintf',
  1458. 'mlt_mindf' => 'mlt.mindf',
  1459. 'mlt_minwl' => 'mlt.minwl',
  1460. 'mlt_maxwl' => 'mlt.maxwl',
  1461. 'mlt_maxqt' => 'mlt.maxqt',
  1462. 'mlt_boost' => 'mlt.boost',
  1463. 'mlt_qf' => 'mlt.qf',
  1464. );
  1465. $params = array(
  1466. 'qt' => 'mlt',
  1467. 'fl' => 'nid,title,path,url',
  1468. 'mlt.fl' => implode(',', $settings['mlt_fl']),
  1469. );
  1470. foreach ($fields as $form_key => $name) {
  1471. if (!empty($settings[$form_key])) {
  1472. $params[$name] = $settings[$form_key];
  1473. }
  1474. }
  1475. $query = apachesolr_drupal_query('id:' . $id);
  1476. // This hook allows modules to modify the query and params objects.
  1477. apachesolr_modify_query($query, $params, 'apachesolr_mlt');
  1478. if (empty($query)) {
  1479. return;
  1480. }
  1481. $response = $solr->search($query->get_query_basic(), 0, $settings['num_results'], $params);
  1482. if ($response->response) {
  1483. $docs = (array) end($response->response);
  1484. return $docs;
  1485. }
  1486. }
  1487. catch ( Exception $e ) {
  1488. watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), WATCHDOG_ERROR );
  1489. }
  1490. }
  1491. /**
  1492. * Implementation of hook_form_alter
  1493. */
  1494. function apachesolr_form_alter($form_id, &$form) {
  1495. switch ($form_id) {
  1496. /**
  1497. * Wiping CCK fields mappings cache. We cannot invoke, as in D6, a hook
  1498. * on fieldapi like we do in D6 with hook_content_fieldapi(), so we add a
  1499. * new submit callback handler to CCK fields forms.
  1500. */
  1501. case '_content_admin_field':
  1502. case '_content_admin_field_remove':
  1503. case '_content_admin_field_add_existing':
  1504. case '_content_admin_field_add_new':
  1505. $form['#submit']['apachesolr_clear_cck_fields_cache'] = array();
  1506. break;
  1507. case 'block_admin_display_form':
  1508. foreach ($form as $key => $block) {
  1509. if ((strpos($key, "apachesolr_mlt-") === 0) && $block['module']['#value'] == 'apachesolr') {
  1510. $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/settings/apachesolr/mlt/delete_block/'. $block['delta']['#value']));
  1511. }
  1512. }
  1513. break;
  1514. case 'block_admin_configure':
  1515. if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') {
  1516. $form['block_settings']['title']['#access'] = FALSE;
  1517. }
  1518. break;
  1519. }
  1520. }
  1521. /**
  1522. * This function is invoked after a CCK add/edit/delete and is used to clear the cache.
  1523. */
  1524. function apachesolr_clear_cck_fields_cache() {
  1525. cache_clear_all('cck_fields', 'cache_apachesolr');
  1526. }
  1527. /**
  1528. * Returns a list of blocks. Used by hook_block
  1529. */
  1530. function apachesolr_mlt_list_blocks() {
  1531. $blocks = variable_get('apachesolr_mlt_blocks', array());
  1532. foreach ($blocks as $delta => $settings) {
  1533. $blocks[$delta] += array('info' => t('Apache Solr recommendations: !name', array('!name' => $settings['name'])) , 'cache' => BLOCK_CACHE_PER_PAGE);
  1534. }
  1535. return $blocks;
  1536. }
  1537. function apachesolr_mlt_load_block($delta) {
  1538. $blocks = variable_get('apachesolr_mlt_blocks', array());
  1539. return isset($blocks[$delta]) ? $blocks[$delta] : FALSE;
  1540. }
  1541. function theme_apachesolr_mlt_recommendation_block($docs) {
  1542. $links = array();
  1543. foreach ($docs as $result) {
  1544. // Suitable for single-site mode.
  1545. $links[] = l($result->title, $result->path, array('html' => TRUE));
  1546. }
  1547. return theme('item_list', $links);
  1548. }
  1549. function theme_apachesolr_facet_link($facet_text, $path, $options = array(), $count, $active = FALSE, $num_found = NULL) {
  1550. $options['attributes']['class'][] = 'apachesolr-facet';
  1551. if ($active) {
  1552. $options['attributes']['class'][] = 'active';
  1553. }
  1554. $options['attributes']['class'] = implode(' ', $options['attributes']['class']);
  1555. return apachesolr_l($facet_text ." ($count)", $path, $options);
  1556. }
  1557. /**
  1558. * A replacement for l()
  1559. * - doesn't add the 'active' class
  1560. * - retains all $_GET parameters that ApacheSolr may not be aware of
  1561. * - if set, $options['query'] MUST be an array
  1562. *
  1563. * @see http://api.drupal.org/api/function/l/6 for parameters and options.
  1564. *
  1565. * @return
  1566. * an HTML string containing a link to the given path.
  1567. */
  1568. function apachesolr_l($text, $path, $options = array()) {
  1569. // Merge in defaults.
  1570. $options += array(
  1571. 'attributes' => array(),
  1572. 'html' => FALSE,
  1573. 'query' => array(),
  1574. );
  1575. // Don't need this, and just to be safe.
  1576. unset($options['attributes']['title']);
  1577. // Double encode + characters for clean URL Apache quirks.
  1578. if (variable_get('clean_url', '0')) {
  1579. $path = str_replace('+', '%2B', $path);
  1580. }
  1581. // Retain GET parameters that ApacheSolr knows nothing about.
  1582. $query = apachesolr_current_query();
  1583. $get = array_diff_key($_GET, array('q' => 1, 'page' => 1), $options['query'], $query->get_url_queryvalues());
  1584. $options['query'] += $get;
  1585. $options_query = drupal_query_string_encode($options['query']);
  1586. $url = $options_query ? url($path, $options_query): url($path);
  1587. return '<a href="'. check_url($url) .'"'. drupal_attributes($options['attributes']) .'>'. ($options['html'] ? $text : check_plain(html_entity_decode($text))) .'</a>';
  1588. }
  1589. function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) {
  1590. if (empty($options['html'])) {
  1591. $facet_text = check_plain(html_entity_decode($facet_text));
  1592. }
  1593. else {
  1594. // Don't pass this option as TRUE into apachesolr_l().
  1595. unset($options['html']);
  1596. }
  1597. $options['attributes']['class'] = 'apachesolr-unclick';
  1598. return apachesolr_l("(-)", $path, $options) . ' '. $facet_text;
  1599. }
  1600. function theme_apachesolr_sort_link($text, $path, $options = array(), $active = FALSE, $direction = '') {
  1601. $icon = '';
  1602. if ($direction) {
  1603. $icon = ' '. theme('tablesort_indicator', $direction);
  1604. }
  1605. if ($active) {
  1606. if (isset($options['attributes']['class'])) {
  1607. $options['attributes']['class'] .= ' active';
  1608. }
  1609. else {
  1610. $options['attributes']['class'] = 'active';
  1611. }
  1612. }
  1613. return $icon . apachesolr_l($text, $path, $options);
  1614. }
  1615. function theme_apachesolr_facet_list($items, $display_limit = 0) {
  1616. // theme('item_list') expects a numerically indexed array.
  1617. $items = array_values($items);
  1618. // If there is a limit and the facet count is over the limit, hide the rest.
  1619. if (($display_limit > 0) && (count($items) > $display_limit)) {
  1620. // Show/hide extra facets.
  1621. drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js');
  1622. // Add translated strings.
  1623. drupal_add_js(array('apachesolr' => array('showMore' => t('Show more'), 'showFewer' => t('Show fewer'))), 'setting');
  1624. // Split items array into displayed and hidden.
  1625. $hidden_items = array_splice($items, $display_limit);
  1626. foreach ($hidden_items as $hidden_item) {
  1627. if (!is_array($hidden_item)) {
  1628. $hidden_item = array('data' => $hidden_item);
  1629. }
  1630. $items[] = $hidden_item + array('class' => 'apachesolr-hidden-facet');
  1631. }
  1632. }
  1633. $admin_link = '';
  1634. if (user_access('administer search')) {
  1635. $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters');
  1636. }
  1637. return theme('item_list', $items) . $admin_link;
  1638. }
  1639. function theme_apachesolr_sort_list($items) {
  1640. // theme('item_list') expects a numerically indexed array.
  1641. $items = array_values($items);
  1642. return theme('item_list', $items);
  1643. }
  1644. /**
  1645. * The interface for all 'query' objects.
  1646. */
  1647. interface Drupal_Solr_Query_Interface {
  1648. /**
  1649. * Checks to see if a specific filter is already present.
  1650. *
  1651. * @param string $field
  1652. * the facet field to check
  1653. *
  1654. * @param string $value
  1655. * The facet value to check against
  1656. */
  1657. function has_filter($field, $value);
  1658. /**
  1659. * Remove a filter from the query
  1660. *
  1661. * @param string $field
  1662. * the facet field to remove
  1663. *
  1664. * @param string $value
  1665. * The facet value to remove
  1666. * This value can be NULL
  1667. */
  1668. function remove_filter($field, $value = NULL);
  1669. /**
  1670. * Add a filter to a query
  1671. *
  1672. * @param string $field
  1673. * the facet field to apply to this query
  1674. *
  1675. * @param string value
  1676. * the value of the facet to apply
  1677. *
  1678. * @param boolean $exclude
  1679. * Optional paramter. If TRUE, the filter will be negative,
  1680. * meaning that matching values will be excluded from the
  1681. * result set.
  1682. */
  1683. function add_filter($field, $value, $exclude = FALSE);
  1684. /**
  1685. * Return the search path (including the search keywords).
  1686. */
  1687. function get_path();
  1688. /**
  1689. * Return an array of parameters for use in the l function.
  1690. *
  1691. * @see l()
  1692. */
  1693. function get_url_queryvalues();
  1694. /**
  1695. * return the basic string query
  1696. */
  1697. function get_query_basic();
  1698. /**
  1699. * Set the solrsort.
  1700. *
  1701. * @param $field
  1702. * The name of a field in the solr index that's an allowed sort.
  1703. *
  1704. * @param $direction
  1705. * 'asc' or 'desc'
  1706. */
  1707. function set_solrsort($field, $direction);
  1708. /**
  1709. * Get the solrsort.
  1710. *
  1711. * Returns the non-urlencode, non-aliased sort field and direction.
  1712. * as an array keyed with '#name' and '#direction'.
  1713. */
  1714. function get_solrsort();
  1715. /**
  1716. * Return an array of all filters.
  1717. */
  1718. function get_filters($name = NULL);
  1719. /**
  1720. * Add a subquery to the query.
  1721. *
  1722. * @param Drupal_Solr_Query_Interface $query
  1723. * The query to add to the orginal query - may have keywords or filters.
  1724. *
  1725. * @param string $fq_operator
  1726. * The operator to use within the filter part of the subquery
  1727. *
  1728. * @param string $q_operator
  1729. * The operator to use in joining the subquery to
  1730. * the main keywords. Note - this is unlikely to work
  1731. * with the Dismax handler when the main query is only
  1732. * keywords.
  1733. */
  1734. function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND');
  1735. /**
  1736. * return the sorts that are provided by the query object
  1737. *
  1738. * @return array all the sorts provided
  1739. */
  1740. function get_available_sorts();
  1741. /**
  1742. * Remove a specific subquery
  1743. *
  1744. * @param Drupal_Solr_Query_Interface $query
  1745. * the query to remove
  1746. */
  1747. function remove_subquery(Drupal_Solr_Query_Interface $query);
  1748. /**
  1749. * remove all subqueries
  1750. */
  1751. function remove_subqueries();
  1752. /**
  1753. * make a sort available
  1754. */
  1755. function set_available_sort($field, $sort);
  1756. }
  1757. /**
  1758. * Wrapper function for tt() if i18nstrings enabled.
  1759. */
  1760. function apachesolr_tt($name, $string, $langcode = NULL, $update = FALSE) {
  1761. if (module_exists('i18nstrings')) {
  1762. return tt($name, $string, $langcode, $update);
  1763. }
  1764. else {
  1765. return $string;
  1766. }
  1767. }