diff --git a/CHANGELOG.txt b/CHANGELOG.txt index af582151..9b0bcfe2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,30 @@ -Search API Solr search 1.x, dev (xx/xx/xxxx): ---------------------------------------------- +Search API Solr search 1.4 (12/25/2013): +---------------------------------------- +- #2157839 by drunken monkey, Nick_vh: Updated config files to the newest + version. +- #2130827 by drunken monkey: Added additional Solr server information to the + Server overview. +- #2126281 by drunken monkey: Update error handling according to the latest + Search API change. +- #2127991 by drunken monkey: Fixed handling of negated fulltext keys. +- #2113943 by drunken monkey: Fixed clash in specifying the HTTP method for + searches. +- #2127193 by jlapp: Fixed date field values returned for multi-index searches. +- #2122155 drunken monkey: Added the "Files" tab to contextual links. +- #1846860 by andrewbelcher, drclaw, drunken monkey, danielnolde: Added a way + to easily define new dynamic field types. +- #2064377 by Nick_vh: Made configuration files compatible with Solr Cloud. +- #2107417 by Nick_vh: Fixed config files for Solr 4.5. + +Search API Solr search 1.3 (10/23/2013): +---------------------------------------- +- #2099683 by drunken monkey: Added support for 'virtual fields' in Views. +- #1997702 by ianthomas_uk, drunken monkey: Added "AUTO" mode for HTTP method. +- #2033913 by drunken monkey: Fixed small error in schema.xml. +- #2073441 by drunken monkey: Removed custom uninstall code for deleting + dependent servers. +- #1882190 by corvus_ch, arnested, drunken monkey: Added optional index ID + prefixes. Search API Solr search 1.2 (09/01/2013): ---------------------------------------- diff --git a/README.txt b/README.txt index 192bdad8..8c1443a1 100644 --- a/README.txt +++ b/README.txt @@ -113,6 +113,23 @@ Hidden variables By default, keywords that occur in more than 90% of results are ignored for autocomplete suggestions. This setting lets you modify that behaviour by providing your own ratio. Use 1 or greater to use all suggestions. +- search_api_solr_index_prefix (default: '') + By default, the index ID in the Solr server is the same as the index's machine + name in Drupal. This setting will let you specify a prefix for the index IDs + on this Drupal installation. Only use alphanumeric characters and underscores. + Since changing the prefix makes the currently indexed data inaccessible, you + should change this vairable only when no indexes are currently on any Solr + servers. +- search_api_solr_index_prefix_INDEX_ID (default: '') + Same as above, but a per-index prefix. Use the index's machine name as + INDEX_ID in the variable name. Per-index prefixing is done before the global + prefix is added, so the global prefix will come first in the final name: + (GLOBAL_PREFIX)(INDEX_PREFIX)(INDEX_ID) + The same rules as above apply for setting the prefix. +- search_api_solr_http_get_max_length (default: 4000) + The maximum number of bytes that can be handled as an HTTP GET query when + HTTP method is AUTO. Typically Solr can handle up to 65355 bytes, but Tomcat + and Jetty will error at slightly less than 4096 bytes. Customizing your Solr server ---------------------------- diff --git a/includes/service.inc b/includes/service.inc index 20e57b82..964d262f 100644 --- a/includes/service.inc +++ b/includes/service.inc @@ -79,7 +79,7 @@ class SearchApiSolrService extends SearchApiAbstractService { 'excerpt' => FALSE, 'retrieve_data' => FALSE, 'highlight_data' => FALSE, - 'http_method' => 'POST', + 'http_method' => 'AUTO', // Default to TRUE for new servers, but to FALSE for existing ones. 'clean_ids' => $this->options ? FALSE : TRUE, 'autocorrect_spell' => TRUE, @@ -197,9 +197,10 @@ class SearchApiSolrService extends SearchApiAbstractService { $form['advanced']['http_method'] = array( '#type' => 'select', '#title' => t('HTTP method'), - '#description' => t('The HTTP method to use for sending queries. Usually, POST will work fine in all cases.'), + '#description' => t('The HTTP method to use for sending queries. GET will often fail with larger queries, while POST should not be cached. AUTO will use GET when possible, and POST for queries that are too large.'), '#default_value' => $options['http_method'], '#options' => array( + 'AUTO' => t('AUTO'), 'POST' => 'POST', 'GET' => 'GET', ), @@ -265,55 +266,157 @@ class SearchApiSolrService extends SearchApiAbstractService { } /** - * Overrides SearchApiAbstractService::supportsFeature(). + * {@inheritdoc} */ public function supportsFeature($feature) { - // Search API features. - $supported = array( + // First, check the features we always support. + $supported = drupal_map_assoc(array( 'search_api_autocomplete', 'search_api_facets', 'search_api_facets_operator_or', 'search_api_grouping', 'search_api_mlt', 'search_api_multi', + 'search_api_service_extra', 'search_api_spellcheck', 'search_api_data_type_location', 'search_api_data_type_geohash', - ); - - // Custom data types. - foreach (search_api_solr_get_dynamic_field_info() as $type => $info) { - $supported[] = 'search_api_data_type_' . $type; + )); + if (isset($supported[$feature])) { + return TRUE; } - $supported = drupal_map_assoc($supported); - return isset($supported[$feature]); + // If it is a custom data type, maybe we support it automatically via + // search_api_solr_hook_search_api_data_type_info(). + if (substr($feature, 0, 21) != 'search_api_data_type_') { + return FALSE; + } + $type = substr($feature, 21); + $type = search_api_get_data_type_info($type); + // We only support it if the "prefix" key is set. + return $type && !empty($type['prefix']); } /** * Overrides SearchApiAbstractService::viewSettings(). + * + * Returns an empty string since information is instead added via + * getExtraInformation(). */ public function viewSettings() { - $output = ''; - $options = $this->options; + return ''; + } - $output .= "
\n
"; - $output .= t('Solr server URI'); - $output .= "
\n
"; - $output .= $this->getServerLink(); - $output .= '
'; - if ($options['http_user']) { - $output .= "\n
"; - $output .= t('Basic HTTP authentication'); - $output .= "
\n
"; - $output .= t('Username: @user', array('@user' => $options['http_user'])); - $output .= "
\n
"; - $output .= t('Password: @pass', array('@pass' => str_repeat('*', strlen($options['http_pass'])))); - $output .= '
'; + /** + * {@inheritdoc} + */ + public function getExtraInformation() { + $info = array(); + + $info[] = array( + 'label' => t('Solr server URI'), + 'info' => $this->getServerLink(), + ); + + if ($this->options['http_user']) { + $vars = array( + '@user' => $this->options['http_user'], + '@pass' => str_repeat('*', strlen($this->options['http_pass'])), + ); + $http = t('Username: @user; Password: @pass', $vars); + $info[] = array( + 'label' => t('Basic HTTP authentication'), + 'info' => $http, + ); } - $output .= "\n
"; - return $output; + if ($this->server->enabled) { + // If the server is enabled, check whether Solr can be reached. + $ping = $this->ping(); + if ($ping) { + $msg = t('The Solr server could be reached (latency: @millisecs ms).', array('@millisecs' => $ping * 1000)); + } + else { + $msg = t('The Solr server could not be reached. Further data is therefore unavailable.'); + } + $info[] = array( + 'label' => t('Connection'), + 'info' => $msg, + 'status' => $ping ? 'ok' : 'error', + ); + + if ($ping) { + try { + // If Solr can be reached, provide more information. This isn't done + // often (only when an admin views the server details), so we clear the + // cache to get the current data. + $this->connect(); + $this->solr->clearCache(); + $data = $this->solr->getLuke(); + if (isset($data->index->numDocs)) { + // Collect the stats + $stats_summary = $this->solr->getStatsSummary(); + + $pending_msg = $stats_summary['@pending_docs'] ? t('(@pending_docs sent but not yet processed)', $stats_summary) : ''; + $index_msg = $stats_summary['@index_size'] ? t('(@index_size on disk)', $stats_summary) : ''; + $indexed_message = t('@num items !pending !index_msg', array( + '@num' => $data->index->numDocs, + '!pending' => $pending_msg, + '!index_msg' => $index_msg, + )); + $info[] = array( + 'label' => t('Indexed'), + 'info' => $indexed_message, + ); + + if (!empty($stats_summary['@deletes_total'])) { + $info[] = array( + 'label' => t('Pending Deletions'), + 'info' => $stats_summary['@deletes_total'], + ); + } + + $info[] = array( + 'label' => t('Delay'), + 'info' => t('@autocommit_time before updates are processed.', $stats_summary), + ); + + $status = 'ok'; + if (substr($stats_summary['@schema_version'], 0, 10) == 'search-api') { + drupal_set_message(t('Your schema.xml version is too old. Please replace all configuration files with the ones packaged with this module and re-index you data.'), 'error'); + $status = 'error'; + } + elseif (substr($stats_summary['@schema_version'], 0, 9) != 'drupal-4.') { + $variables['@url'] = url(drupal_get_path('module', 'search_api_solr') . '/INSTALL.txt'); + $message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in the INSTALL.txt file for setting up Solr.', $variables); + drupal_set_message($message, 'error'); + $status = 'error'; + } + $info[] = array( + 'label' => t('Schema'), + 'info' => $stats_summary['@schema_version'], + 'status' => $status, + ); + + if (!empty($stats_summary['@core_name'])) { + $info[] = array( + 'label' => t('Solr Core Name'), + 'info' => $stats_summary['@core_name'], + ); + } + } + } + catch (SearchApiException $e) { + $info[] = array( + 'label' => t('Additional information'), + 'info' => t('An error occurred while trying to retrieve additional information from the Solr server: @msg.', array('@msg' => $e->getMessage())), + 'status' => 'error', + ); + } + } + } + + return $info; } /** @@ -377,12 +480,12 @@ class SearchApiSolrService extends SearchApiAbstractService { $id = is_object($index) ? $index->machine_name : $index; // Only delete the index's data if the index isn't read-only. if (!is_object($index) || empty($index->read_only)) { + $this->connect(); try { - $this->connect(); - $this->solr->deleteByQuery("index_id:" . $id); + $this->solr->deleteByQuery("index_id:" . $this->getIndexId($id)); } catch (Exception $e) { - watchdog_exception('search_api_solr', $e, "%type while deleting an index's data: !message in %function (line %line of %file)."); + throw new SearchApiException($e->getMessage()); } } } @@ -393,7 +496,7 @@ class SearchApiSolrService extends SearchApiAbstractService { public function indexItems(SearchApiIndex $index, array $items) { $documents = array(); $ret = array(); - $index_id = $index->machine_name; + $index_id = $this->getIndexId($index->machine_name); $fields = $this->getFieldNames($index); foreach ($items as $id => $item) { @@ -481,10 +584,10 @@ class SearchApiSolrService extends SearchApiAbstractService { } $inner_type = search_api_extract_inner_type($type); - $type_info = search_api_solr_get_dynamic_field_info($inner_type); + $type_info = search_api_solr_get_data_type_info($inner_type); $pref = isset($type_info['prefix']) ? $type_info['prefix']: ''; if (empty($type_info['always multiValued'])) { - $pref .= $type == $inner_type ? 's' : 'm'; + $pref .= ($type == $inner_type) ? 's' : 'm'; } if (!empty($this->options['clean_ids'])) { $name = $pref . '_' . str_replace(':', '$', $key); @@ -586,33 +689,28 @@ class SearchApiSolrService extends SearchApiAbstractService { * called in this fashion. */ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) { - try { - $this->connect(); - if ($index) { - $index_id = $index->machine_name; - if (is_array($ids)) { - $solr_ids = array(); - foreach ($ids as $id) { - $solr_ids[] = $this->createId($index_id, $id); - } - $this->solr->deleteByMultipleIds($solr_ids); - } - elseif ($ids == 'all') { - $this->solr->deleteByQuery("index_id:" . $index_id); - } - else { - $this->solr->deleteByQuery("index_id:" . $index_id . ' (' . $ids . ')'); + $this->connect(); + if ($index) { + $index_id = $this->getIndexId($index->machine_name); + if (is_array($ids)) { + $solr_ids = array(); + foreach ($ids as $id) { + $solr_ids[] = $this->createId($index_id, $id); } + $this->solr->deleteByMultipleIds($solr_ids); + } + elseif ($ids == 'all') { + $this->solr->deleteByQuery("index_id:" . $index_id); } else { - $q = $ids == 'all' ? '*:*' : $ids; - $this->solr->deleteByQuery($q); + $this->solr->deleteByQuery("index_id:" . $index_id . ' (' . $ids . ')'); } - $this->scheduleCommit(); } - catch(SearchApiException $e) { - watchdog_exception('search_api_solr', $e, '%type while deleting items from server @server: !message in %function (line %line of %file).', array('@server' => $this->server->name)); + else { + $q = $ids == 'all' ? '*:*' : $ids; + $this->solr->deleteByQuery($q); } + $this->scheduleCommit(); } /** @@ -624,6 +722,7 @@ class SearchApiSolrService extends SearchApiAbstractService { $this->request_handler = NULL; // Get field information. $index = $query->getIndex(); + $index_id = $this->getIndexId($index->machine_name); $fields = $this->getFieldNames($index); // Get Solr connection. $this->connect(); @@ -650,7 +749,7 @@ class SearchApiSolrService extends SearchApiAbstractService { // Extract filters. $filter = $query->getFilter(); $fq = $this->createFilterQueries($filter, $fields, $index->options['fields']); - $fq[] = 'index_id:' . $index->machine_name; + $fq[] = 'index_id:' . $index_id; // Extract sort. $sort = array(); @@ -690,7 +789,7 @@ class SearchApiSolrService extends SearchApiAbstractService { } } $mlt_params['mlt.fl'] = implode(',', $mlt_fl); - $id = $this->createId($index->machine_name, $mlt['id']); + $id = $this->createId($index_id, $mlt['id']); $id = call_user_func(array($this->connection_class, 'phrase'), $id); $keys = 'id:' . $id; } @@ -884,7 +983,7 @@ class SearchApiSolrService extends SearchApiAbstractService { $params['fl'] = '*,score'; } // Retrieve http method from server options. - $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST'; + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; $call_args = array( 'query' => &$keys, @@ -1020,7 +1119,8 @@ class SearchApiSolrService extends SearchApiAbstractService { $result['id'] = $result['fields']['search_api_id']; $result['score'] = $result['fields']['search_api_relevance']; - $solr_id = $this->createId($index->machine_name, $result['id']); + $index_id = $this->getIndexId($index->machine_name); + $solr_id = $this->createId($index_id, $result['id']); $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields); if ($excerpt) { $result['excerpt'] = $excerpt; @@ -1248,7 +1348,7 @@ class SearchApiSolrService extends SearchApiAbstractService { // #conjunction | #negation | return value // ---------------------------------------------------------------- // AND | FALSE | A B C - // AND | TRUE | -(A B C) + // AND | TRUE | -(A AND B AND C) // OR | FALSE | ((A) OR (B) OR (C)) // OR | TRUE | -A -B -C @@ -1264,7 +1364,7 @@ class SearchApiSolrService extends SearchApiAbstractService { } return '((' . implode(') OR (', $k) . '))'; } - $k = implode(' ', $k); + $k = implode($neg ? ' AND ' : ' ', $k); return $neg ? "*:* AND -($k)" : $k; } @@ -1565,7 +1665,7 @@ class SearchApiSolrService extends SearchApiAbstractService { // Extract filters $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']); - $fq[] = 'index_id:' . $index->machine_name; + $fq[] = 'index_id:' . $this->getIndexId($index->machine_name); // Autocomplete magic $facet_fields = array(); @@ -1588,7 +1688,7 @@ class SearchApiSolrService extends SearchApiAbstractService { 'spellcheck.count' => 1, ); // Retrieve http method from server options. - $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST'; + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; $call_args = array( 'query' => &$keys, @@ -1728,11 +1828,11 @@ class SearchApiSolrService extends SearchApiAbstractService { 'type' => 'string', ), ); - foreach ($query->getIndexes() as $index_id => $index) { + foreach ($query->getIndexes() as $index) { if (empty($index->options['fields'])) { continue; } - $prefix = $index_id . ':'; + $prefix = $this->getIndexId($index->machine_name) . ':'; foreach ($this->getFieldNames($index) as $field => $key) { if (!isset($solr_fields[$field])) { $solr_fields[$prefix . $field] = $key; @@ -1762,7 +1862,8 @@ class SearchApiSolrService extends SearchApiAbstractService { // Restrict search to searched indexes. $index_filter = array(); - foreach ($query->getIndexes() as $index_id => $index) { + foreach ($query->getIndexes() as $index) { + $index_id = $this->getIndexId($index->machine_name); $index_filter[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id); } $fq[] = implode(' OR ', $index_filter); @@ -1814,7 +1915,7 @@ class SearchApiSolrService extends SearchApiAbstractService { } // Retrieve http method from server options. - $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST'; + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; // Send search request $time_processing_done = microtime(TRUE); @@ -1879,7 +1980,7 @@ class SearchApiSolrService extends SearchApiAbstractService { $terms = clone $terms; unset($terms->_empty_); } - $type = isset($index->options['fields'][$info['field']]['type']) ? search_api_extract_inner_type($index->options['fields'][$info['field']]['type']) : 'string'; + $type = isset($fields[$info['field']]['type']) ? search_api_extract_inner_type($fields[$info['field']]['type']) : 'string'; foreach ($terms as $term => $count) { if ($count >= $min_count) { if ($term === '_empty_') { @@ -2041,4 +2142,25 @@ class SearchApiSolrService extends SearchApiAbstractService { return $this->solr->makeServletRequest($file_servlet_name, $params); } + /** + * Prefixes an index ID as configured. + * + * The resulting ID will be a concatenation of the following strings: + * - If set, the "search_api_solr_index_prefix" variable. + * - If set, the index-specific "search_api_solr_index_prefix_INDEX" variable. + * - The index's machine name. + * + * @param string $machine_name + * The index's machine name. + * + * @return string + * The prefixed machine name. + */ + protected function getIndexId($machine_name) { + // Prepend per-index prefix. + $id = variable_get('search_api_solr_index_prefix_' . $machine_name, '') . $machine_name; + // Prepend environment prefix. + $id = variable_get('search_api_solr_index_prefix', '') . $id; + return $id; + } } diff --git a/includes/solr_connection.inc b/includes/solr_connection.inc index 8a262122..6b996471 100644 --- a/includes/solr_connection.inc +++ b/includes/solr_connection.inc @@ -58,10 +58,11 @@ */ /** - * Starting point for the Solr API. Represents a Solr server resource and has - * methods for pinging, adding, deleting, committing, optimizing and searching. + * Represents a Solr server resource. + * + * Contains methods for pinging, adding, deleting, committing, optimizing and + * searching. */ - class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { /** @@ -133,13 +134,6 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { */ protected $update_url; - /** - * The HTTP method to use for search requests. - * - * @var string - */ - protected $method; - /** * HTTP Basic Authentication header to set for requests to the Solr server. * @@ -208,8 +202,6 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { * information to add basic HTTP authentication to all requests to the * Solr server. Not set by default. * - http_pass: See "http_user". - * - http_method: The HTTP method to use for searches. Can be either "GET" - * or "POST". Defaults to "POST". */ public function __construct(array $options) { $options += array( @@ -219,16 +211,12 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { 'path' => 'solr', 'http_user' => NULL, 'http_pass' => NULL, - 'http_method' => 'POST', ); $this->options = $options; $path = '/' . trim($options['path'], '/') . '/'; $this->base_url = $options['scheme'] . '://' . $options['host'] . ':' . $options['port'] . $path; - // Make sure we always have a valid method set, default to POST. - $this->method = $options['http_method'] == 'GET' ? 'GET' : 'POST'; - // Set HTTP Basic Authentication parameter, if login data was set. if (strlen($options['http_user']) && strlen($options['http_pass'])) { $this->http_auth = 'Basic ' . base64_encode($options['http_user'] . ':' . $options['http_pass']); @@ -429,7 +417,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { } $response = $this->sendRawGet($url); $this->stats = simplexml_load_string($response->data); - if ($this->env_id) { + if ($cid) { cache_set($cid, $response->data, 'cache_search_api_solr'); } } @@ -813,15 +801,15 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { * * Will be synchronous unless $waitSearcher is set to FALSE. * - * @param $type + * @param string $type * Either "commit" or "optimize". - * @param $waitSearcher + * @param bool $waitSearcher * (optional) Wait until a new searcher is opened and registered as the main * query searcher, making the changes visible. Defaults to true. - * @param $timeout + * @param int $timeout * Seconds to wait until timing out with an exception. Defaults to an hour. * - * @return + * @return object * A response object. * * @throws SearchApiException @@ -845,7 +833,20 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { } /** - * Like PHP's built in http_build_query(), but uses rawurlencode() and no [] for repeated params. + * Generates an URL-encoded query string. + * + * Works like PHP's built in http_build_query() (or drupal_http_build_query()) + * but uses rawurlencode() and no [] for repeated params, to be compatible + * with the Java-based servers Solr runs on. + * + * + * @param array $query + * The query parameters which should be set. + * @param string $parent + * Internal use only. + * + * @return string + * A query string to append (after "?") to a URL. */ protected function httpBuildQuery(array $query, $parent = '') { $params = array(); @@ -870,7 +871,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { } /** - * Implements SearchApiSolrConnectionInterface::search(). + * {@inheritdoc} */ public function search($query = NULL, array $params = array(), $method = 'GET') { // Always use JSON. See @@ -887,15 +888,18 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { // PHP's built-in http_build_query() doesn't give us the format Solr wants. $queryString = $this->httpBuildQuery($params); - if ($this->method == 'GET') { + if ($method == 'GET' || $method == 'AUTO') { $searchUrl = $this->constructUrl(self::SEARCH_SERVLET, array(), $queryString); - return $this->sendRawGet($searchUrl); - } - else if ($this->method == 'POST') { - $searchUrl = $this->constructUrl(self::SEARCH_SERVLET); - $options['data'] = $queryString; - $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; - return $this->sendRawPost($searchUrl, $options); + if ($method == 'GET' || strlen($searchUrl) <= variable_get('search_api_solr_http_get_max_length', 4000)) { + return $this->sendRawGet($searchUrl); + } } + + // Method is POST, or AUTO with a long query + $searchUrl = $this->constructUrl(self::SEARCH_SERVLET); + $options['data'] = $queryString; + $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + return $this->sendRawPost($searchUrl, $options); } + } diff --git a/includes/solr_connection.interface.inc b/includes/solr_connection.interface.inc index df30adef..4a42db80 100644 --- a/includes/solr_connection.interface.inc +++ b/includes/solr_connection.interface.inc @@ -71,8 +71,8 @@ interface SearchApiSolrConnectionInterface { /** * Gets information about the Solr Core. * - * @return array - * An array with system information. + * @return object + * A response object with system information. */ public function getSystemInfo(); @@ -288,8 +288,8 @@ interface SearchApiSolrConnectionInterface { * documentation). Use arrays for parameter keys used more than once (e.g., * facet.field). * @param string $method - * The HTTP method to use. Must be either "GET" or "POST". Defaults to - * "GET". + * The HTTP method to use. Must be either "GET", "POST" or "AUTO". Defaults + * to "GET". * * @return object * A response object. diff --git a/search_api_solr.api.php b/search_api_solr.api.php index cc9b2084..99b93b3b 100644 --- a/search_api_solr.api.php +++ b/search_api_solr.api.php @@ -101,45 +101,50 @@ function hook_search_api_solr_multi_query_alter(array &$call_args, SearchApiMult } /** - * Define how Search API Solr should index different data types. + * Provide Solr dynamic fields as Search API data types. * - * It is important to make sure that any types you define are also declared to - * Search API using hook_search_api_data_type_info(). + * This serves as a placeholder for documenting additional keys for + * hook_search_api_data_type_info() which are recognized by this module to + * automatically support dynamic field types from the schema. * * @return array - * An array containing data type definitions, keyed by their type identifier - * and containing the following keys: - * - prefix: The prefix used by the dynamic field type. - * - always multiValued: (optional) Whether the single/multiple prefix should - * be skipped for this data type. Defaults to FALSE. + * In addition to the keys for the individual types that are defined by + * hook_search_api_data_type_info(), the following keys are regonized: + * - prefix: The Solr field name prefix to use for this type. Should match + * two existing dynamic fields definitions with names "{PREFIX}s_*" and + * "{PREFIX}m_*". + * - always multiValued: (optional) If TRUE, only the dynamic field name + * prefix (without the "_*" portion) with multiValued="true" should be given + * by "prefix", instead of the common prefix part for both the single-valued + * and the multi-valued field. This should be the case for all fulltext + * fields, since they might already be tokenized by the Search API. Defaults + * to FALSE. * - * @see hook_search_api_solr_dynamic_field_info_alter() - * @see search_api_solr_get_dynamic_field_info() - * @see hook_search_api_data_type_info(). + *@see hook_search_api_data_type_info() */ -function hook_search_api_solr_dynamic_field_info() { +function search_api_solr_hook_search_api_data_type_info() { return array( - 'example_type' => array( - 'prefix' => 'ex', - // Could be omitted, as FALSE is the default. - 'always multiValued' => FALSE, + // You can use any identifier you want here, but it makes sense to use the + // field type name from schema.xml. + 'edge_n2_kw_text' => array( + // Stock hook_search_api_data_type_info() info: + 'name' => t('Fulltext (w/ partial matching)'), + 'fallback' => 'text', + // Dynamic field with name="te_*". + 'prefix' => 'te', + // Fulltext types should always be multi-valued. + 'always multiValued' => TRUE, + ), + 'tlong' => array( + // Stock hook_search_api_data_type_info() info: + 'name' => t('TrieLong'), + 'fallback' => 'integer', + // Dynamic fields with name="its_*" and name="itm_*". + 'prefix' => 'it', ), ); } -/** - * Alter the data type indexing info. - * - * @param array $infos - * The item type info array, keyed by type identifier. - * - * @see hook_search_api_solr_dynamic_field_info() - */ -function hook_search_api_solr_dynamic_field_info_alter(array &$infos) { - // Change the prefix used for example_type. - $info['example_type']['prefix'] = 'ex2'; -} - /** * @} End of "addtogroup hooks". */ diff --git a/search_api_solr.info b/search_api_solr.info index 1e6caba6..80ce33a5 100644 --- a/search_api_solr.info +++ b/search_api_solr.info @@ -11,9 +11,9 @@ files[] = includes/solr_connection.interface.inc files[] = includes/solr_field.inc files[] = includes/spellcheck.inc -; Information added by drupal.org packaging script on 2013-09-01 -version = "7.x-1.2" +; Information added by Drupal.org packaging script on 2013-12-25 +version = "7.x-1.4" core = "7.x" project = "search_api_solr" -datestamp = "1378026413" +datestamp = "1387970905" diff --git a/search_api_solr.install b/search_api_solr.install index c541a93d..d67bb4d4 100644 --- a/search_api_solr.install +++ b/search_api_solr.install @@ -59,13 +59,10 @@ function search_api_solr_requirements($phase) { * Implements hook_uninstall(). */ function search_api_solr_uninstall() { - if (module_exists('search_api')) { - db_delete('search_api_server') - ->condition('class', 'search_api_solr_service') - ->execute(); - } variable_del('search_api_solr_last_optimize'); variable_del('search_api_solr_autocomplete_max_occurrences'); + variable_del('search_api_solr_index_prefix'); + variable_del('search_api_solr_http_get_max_length'); } /** diff --git a/search_api_solr.module b/search_api_solr.module index 52ae956d..c98e4c3f 100644 --- a/search_api_solr.module +++ b/search_api_solr.module @@ -14,11 +14,12 @@ function search_api_solr_menu() { 'description' => 'View Solr configuration files.', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_api_solr_solr_config_form', 5), - 'access callback' => 'search_api_access_server_files', + 'access callback' => 'search_api_solr_access_server_files', 'access arguments' => array(5), 'file' => 'search_api_solr.admin.inc', 'type' => MENU_LOCAL_TASK, 'weight' => -1, + 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, ); return $items; @@ -28,15 +29,18 @@ function search_api_solr_menu() { * Implements hook_search_api_service_info(). */ function search_api_solr_search_api_service_info() { + $variables = array( + '@solr_wiki_url' => url('http://wiki.apache.org/solr/SolrQuerySyntax'), + '@readme_url' => url(drupal_get_path('module', 'search_api_solr') . '/README.txt'), + ); $services['search_api_solr_service'] = array( 'name' => t('Solr service'), - 'description' => t('

Index items using an Apache Solr search server.

' . - '', - array('@url' => url('http://wiki.apache.org/solr/SolrQuerySyntax'))), + 'description' => t('

Index items using an Apache Solr search server.

+', $variables), 'class' => 'SearchApiSolrService', ); return $services; @@ -58,20 +62,6 @@ function search_api_solr_help($path, array $arg = array()) { } } } - elseif ($path == 'admin/config/search/search_api/server/%' && !empty($arg[5])) { - $server = search_api_server_load($arg[5]); - if ($server && $server->enabled && $server->class == 'search_api_solr_service') { - $ping = $server->ping(); - $type = $ping ? 'status' : 'error'; - if ($ping) { - $msg = t('The Solr server could be reached (latency: @millisecs ms).', array('@millisecs' => $ping * 1000)); - } - else { - $msg = t('The Solr server could not be reached.'); - } - drupal_set_message($msg, $type); - } - } } /** @@ -113,83 +103,109 @@ function search_api_solr_search_api_server_update(SearchApiServer $server) { } /** - * Returns either all dynamic field types, or a specific one. - * - * @param $type - * If specified, the type whose definition should be returned. - * - * @return array - * If $type was not given, an array containing all custom dynamic fields, in - * the format specified by hook_search_api_solr_dynamic_field_info(). - * Otherwise, the definition for the given type, or NULL if it is unknown. - * - * @see hook_search_api_solr_dynamic_field_info(). + * Implements hook_views_api(). */ -function search_api_solr_get_dynamic_field_info($type = NULL) { - $types = &drupal_static(__FUNCTION__); - if (!isset($types)) { - $types = module_invoke_all('search_api_solr_dynamic_field_info'); - $types = $types ? $types : array(); - drupal_alter('search_api_solr_dynamic_field_info', $types); +function search_api_solr_views_api() { + if (module_exists('search_api_views')) { + return array( + 'api' => 3, + ); } +} + +/** + * Retrieves Solr-specific data for available data types. + * + * Returns the data type information for both the default Search API data types + * and custom data types defined by hook_search_api_data_type_info(). Names for + * default data types are not included, since they are not relevant to the Solr + * service class. + * + * We're adding some extra Solr field information for the default search api + * data types (as well as on behalf of a couple contrib field types). The + * extra information we're adding is documented in + * search_api_solr_hook_search_api_data_type_info(). You can use the same + * additional keys in hook_search_api_data_type_info() to support custom + * dynamic fields in your indexes with Solr. + * + * @param string|null $type + * (optional) A specific type for which the information should be returned. + * Defaults to returning all information. + * + * @return array|null + * If $type was given, information about that type or NULL if it is unknown. + * Otherwise, an array of all types. The format in both cases is the same as + * for search_api_get_data_type_info(). + * + * @see search_api_get_data_type_info() + * @see search_api_solr_hook_search_api_data_type_info() + */ +function search_api_solr_get_data_type_info($type = NULL) { + $types = &drupal_static(__FUNCTION__); + + if (!isset($types)) { + // Grab the stock search_api data types. + $types = search_api_get_data_type_info(); + + // Add our extras for the default search api fields. + $types += array( + 'text' => array( + 'prefix' => 'tm', + 'always multiValued' => TRUE, + ), + 'string' => array( + 'prefix' => 's', + ), + 'integer' => array( + 'prefix' => 'i', + ), + 'decimal' => array( + 'prefix' => 'f', + ), + 'date' => array( + 'prefix' => 'd', + ), + 'duration' => array( + 'prefix' => 'i', + ), + 'boolean' => array( + 'prefix' => 'b', + ), + 'uri' => array( + 'prefix' => 's', + ), + 'tokens' => array( + 'prefix' => 'tm', + 'always multiValued' => TRUE, + ), + ); + + // Extra data type info. + $extra_types_info = array( + 'location' => array( + 'prefix' => 'loc', + ), + 'geohash' => array( + 'prefix' => 'geo', + ), + ); + + // For the extra types, only add our extra info if it's already been defined. + foreach ($extra_types_info as $key => $info) { + if (array_key_exists($key, $types)) { + // Merge our extras into the data type info + $types[$key] += $info; + } + } + } + + // Return the info. if (isset($type)) { return isset($types[$type]) ? $types[$type] : NULL; } return $types; } -/** - * Implements hook_search_api_solr_dynamic_field_info(). - */ -function search_api_solr_search_api_solr_dynamic_field_info() { - return array( - 'text' => array( - 'prefix' => 'tm', - 'always multiValued' => TRUE, - ), - 'tokens' => array( - 'prefix' => 'tm', - 'always multiValued' => TRUE, - ), - 'string' => array( - 'prefix' => 's', - 'always multiValued' => FALSE, - ), - 'integer' => array( - 'prefix' => 'i', - 'always multiValued' => FALSE, - ), - 'decimal' => array( - 'prefix' => 'f', - 'always multiValued' => FALSE, - ), - 'date' => array( - 'prefix' => 'd', - 'always multiValued' => FALSE, - ), - 'duration' => array( - 'prefix' => 'i', - 'always multiValued' => FALSE, - ), - 'boolean' => array( - 'prefix' => 'b', - 'always multiValued' => FALSE, - ), - 'uri' => array( - 'prefix' => 's', - 'always multiValued' => FALSE, - ), - 'location' => array( - 'prefix' => 'loc', - 'always multiValued' => FALSE, - ), - 'geohash' => array( - 'prefix' => 'geohash', - 'always multiValued' => FALSE, - ), - ); -} - /** * Retrieves a list of all config files of a server. * @@ -229,6 +245,15 @@ function search_api_solr_server_get_files(SearchApiServer $server, $dir_name = N return array_reduce($result, 'array_merge', array()); } +/** + * @deprecated + * + * @see search_api_solr_access_server_files() + */ +function search_api_access_server_files(SearchApiServer $server) { + return search_api_solr_access_server_files($server); +} + /** * Access callback for a server's "Files" tab. * @@ -241,7 +266,7 @@ function search_api_solr_server_get_files(SearchApiServer $server, $dir_name = N * @return bool * TRUE if access should be granted, FALSE otherwise. */ -function search_api_access_server_files(SearchApiServer $server) { +function search_api_solr_access_server_files(SearchApiServer $server) { if (!user_access('administer search_api')) { return FALSE; } diff --git a/search_api_solr.views.inc b/search_api_solr.views.inc new file mode 100644 index 00000000..657b7ace --- /dev/null +++ b/search_api_solr.views.inc @@ -0,0 +1,62 @@ +server(); + if (!$server || empty($server->options['retrieve_data'])) { + return; + } + // Fill in base data. + $key = 'search_api_index_' . $index->machine_name; + $table = & $data[$key]; + + try { + $wrapper = $index->entityWrapper(NULL, FALSE); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_solr', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } + + // Remember fields that aren't added by data alterations, etc. (since + // there isn't any other way to tell them apart). + $normal_fields = array(); + foreach ($wrapper as $key => $property) { + $normal_fields[$key] = TRUE; + } + + try { + $wrapper = $index->entityWrapper(NULL); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_solr', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } + + // Add field handlers for items added by data alterations, etc. + foreach ($wrapper as $key => $property) { + if (empty($normal_fields[$key])) { + $info = $property->info(); + if ($info) { + entity_views_field_definition($key, $info, $table); + } + } + } + } + } + catch (Exception $e) { + watchdog_exception('search_api_views', $e); + } +} diff --git a/solr-conf/1.4/schema.xml b/solr-conf/1.4/schema.xml index 3c7a45ae..f8b7c9dc 100644 --- a/solr-conf/1.4/schema.xml +++ b/solr-conf/1.4/schema.xml @@ -70,8 +70,8 @@ so that range queries work correctly. --> - - + + - + - - + + - + - - + + + - - - ${solr.replication.master:false} - commit - startup - ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} - - - ${solr.replication.slave:false} - ${solr.replication.masterUrl:http://localhost:8983/solr}/replication - ${solr.replication.pollInterval:00:00:60} - - + + + ${solr.replication.master:false} + commit + startup + ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} + + + ${solr.replication.slave:false} + ${solr.replication.masterUrl:http://localhost:8983/solr}/replication + ${solr.replication.pollInterval:00:00:60} + + + textSpell diff --git a/solr-conf/3.x/solrcore.properties b/solr-conf/3.x/solrcore.properties index 54a33c95..bc468862 100644 --- a/solr-conf/3.x/solrcore.properties +++ b/solr-conf/3.x/solrcore.properties @@ -5,8 +5,12 @@ solr.replication.pollInterval=00:00:60 solr.replication.masterUrl=http://localhost:8983/solr solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml solr.mlt.timeAllowed=2000 -# You should not set your luceneVersion to anything lower then your Solr Version. +# You should not set your luceneMatchVersion to anything lower than your Solr +# Version. solr.luceneMatchVersion=LUCENE_35 solr.pinkPony.timeAllowed=-1 +# autoCommit after 10000 docs solr.autoCommit.MaxDocs=10000 +# autoCommit after 2 minutes solr.autoCommit.MaxTime=120000 +solr.contrib.dir=../../contrib diff --git a/solr-conf/3.x/stopwords.txt b/solr-conf/3.x/stopwords.txt index 045f6d10..d7f243e4 100644 --- a/solr-conf/3.x/stopwords.txt +++ b/solr-conf/3.x/stopwords.txt @@ -1,4 +1,4 @@ # Contains words which shouldn't be indexed for fulltext fields, e.g., because -# they're to common. For documentation of the format, see +# they're too common. For documentation of the format, see # http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.StopFilterFactory # (Lines starting with a pound character # are ignored.) diff --git a/solr-conf/4.x/schema.xml b/solr-conf/4.x/schema.xml index 6e8e7961..6e2b615b 100644 --- a/solr-conf/4.x/schema.xml +++ b/solr-conf/4.x/schema.xml @@ -10,7 +10,7 @@ http://wiki.apache.org/solr/SchemaXml --> - + - - + + + + + diff --git a/solr-conf/4.x/solrconfig.xml b/solr-conf/4.x/solrconfig.xml index d408253e..cc5f5225 100644 --- a/solr-conf/4.x/solrconfig.xml +++ b/solr-conf/4.x/solrconfig.xml @@ -20,7 +20,7 @@ For more details about configurations options that may appear in this file, see http://wiki.apache.org/solr/SolrConfigXml. --> - + - - + + + - false - 32 - 10 - + + ${solr.autoSoftCommit.MaxDocs:2000} + ${solr.autoSoftCommit.MaxTime:10000} + + + + + ${solr.data.dir:} + + - + - - - ${solr.replication.master:false} - commit - startup - ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} - - - ${solr.replication.slave:false} - ${solr.replication.masterUrl:http://localhost:8983/solr}/replication - ${solr.replication.pollInterval:00:00:60} - - + + + ${solr.replication.master:false} + commit + startup + ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} + + + ${solr.replication.slave:false} + ${solr.replication.masterUrl:http://localhost:8983/solr}/replication + ${solr.replication.pollInterval:00:00:60} + + + + + + + true + json + true + + + textSpell diff --git a/solr-conf/4.x/solrcore.properties b/solr-conf/4.x/solrcore.properties index a2eb167e..b7f8f6c8 100644 --- a/solr-conf/4.x/solrcore.properties +++ b/solr-conf/4.x/solrcore.properties @@ -5,8 +5,16 @@ solr.replication.pollInterval=00:00:60 solr.replication.masterUrl=http://localhost:8983/solr solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml solr.mlt.timeAllowed=2000 -# You should not set your luceneVersion to anything lower then your Solr Version. +# You should not set your luceneMatchVersion to anything lower than your Solr +# Version. solr.luceneMatchVersion=LUCENE_40 solr.pinkPony.timeAllowed=-1 +# autoCommit after 10000 docs solr.autoCommit.MaxDocs=10000 +# autoCommit after 2 minutes solr.autoCommit.MaxTime=120000 +# autoSoftCommit after 2000 docs +solr.autoSoftCommit.MaxDocs=2000 +# autoSoftCommit after 10 seconds +solr.autoSoftCommit.MaxTime=10000 +solr.contrib.dir=../../../contrib