From d16c38f7acd94bd5a79c05b81173b6eaf26fd755 Mon Sep 17 00:00:00 2001 From: Katherine Bailey Date: Fri, 17 Jun 2011 14:22:12 -0700 Subject: [PATCH] Issue #1182614: Facetapi integration --- plugins/facetapi/adapter.inc | 180 ++++++++++++++++++++++++++++++++++ plugins/facetapi/query_type_term.inc | 82 +++++++++++++++ search_api.info | 2 + search_api.module | 165 +++++++++++++++++++++++++++++++ 4 files changed, 429 insertions(+), 0 deletions(-) create mode 100644 plugins/facetapi/adapter.inc create mode 100644 plugins/facetapi/query_type_term.inc diff --git plugins/facetapi/adapter.inc plugins/facetapi/adapter.inc new file mode 100644 index 0000000..31957ed --- /dev/null +++ plugins/facetapi/adapter.inc @@ -0,0 +1,180 @@ +info['instance']; + return $base_path . '/index/'. $index .'/facets/' . $realm_name; + } + + /** + * Allows the backend to initialize its query object before adding the facet + * filters. + * + * @param mixed $query + * The backend's native object. + */ + function initActiveFilters($query) { + $search_id = $query->getOption('search id'); + $facets = facetapi_get_enabled_facets($this->info['instance']); + $fields = array(); + + // Looked to search_api_facets_search_api_query_alter() for guidance here. + $active = &drupal_static('search_api_facetapi_active_facets', array()); + foreach ($facets as $facet) { + $options = $this->getFacet($facet)->getSettings()->settings; + // The 'default_true' option is a choice between "show on all but the + // selected searches" (TRUE) and "show for only the selected searches" + $default_true = isset($options['default_true']) ? $options['default_true'] : TRUE; + // The 'facet_search_ids' option is the list of selected searches that will + // either be excluded or for which the facet will exclusively be displayed. + $facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(); + + if (array_search($search_id, $facet_search_ids) === FALSE) { + $search_ids = variable_get('search_api_facets_search_ids', array()); + if (empty($search_ids[$search_id])) { + // Remember this search ID. + $search_ids[$search_id] = $search_id; + variable_set('search_api_facets_search_ids', $search_ids); + } + if (!$default_true) { + continue; // We are only to show facets for explicitly named search ids. + } + } + elseif ($default_true) { + continue; // The 'facet_search_ids' in the settings are to be excluded. + } + $active[$facet['name']] = $search_id; + $fields[$facet['name']] = array( + 'field' => $facet['field'], + // copying these from the default options specified in the constructor + // for SearchApiFacet in search_api_facet.entity.inc. + // TODO: Proper mapping between the settings as supplied by facet API and + // the settings as used by the searcher. + 'limit' => 10, + 'display_more_link' => FALSE, + 'more_limit' => 10, + 'min_count' => 1, + 'sort' => 'count', + 'missing' => $facet['facet missing allowed'], + 'show_active' => TRUE, + 'default_true' => TRUE, + 'ids_list' => array(), + 'type' => '', + ); + } + + if ($fields) { + $old = $query->setOption('search_api_facets', $fields); + if ($old) { // This will only happen if other modules add facets of their own. + $query->setOption('search_api_facets', $fields + $old); + } + } + } + + /** + * Returns a boolean flagging whether $this->_searcher executed a search. + */ + public function searchExecuted() { + $searches = search_api_current_search(); + return count($searches) ? TRUE : FALSE; + } + + public function suppressOutput($realm_name) { + // Not sure under what circumstances the output will need to be suppressed, + // hand off to the search backend module...? + //return module_invoke($this->info['module'], 'search_api_suppress_output'); + return FALSE; + } + + /** + * Returns the search keys. + */ + public function getSearchKeys() { + + $searches = search_api_current_search(); + // For now, just taking the first current search :-/ + $first = key($searches); + $query = $searches[$first][0]; + $keys = $query->getOriginalKeys(); + if (is_array($keys)) { + $keys = implode(' ', $keys); + } + return $keys; + } + + /** + * Returns the number of total results found for the current search. + */ + public function getResultCount() { + + // Again, I am clueless about the notion of having more than one current + // search and am just working with the first one. + $searches = search_api_current_search(); + $first = key($searches); + // Each search is an array with the query as the first element and the results + // array as the second. + if (isset($searches[$first][1])) { + return $searches[$first][1]['result count']; + } + return 0; + } + + /** + * Allows for backend specific overrides to the settings form. + */ + public function settingsForm(&$form, &$form_state) { + $search_ids = variable_get('search_api_facets_search_ids', array()); + $facet = $form['#facetapi']['facet']; + $realm = $form['#facetapi']['realm']; + $facet_settings = $this->getFacet($facet)->getSettings(); + $options = $facet_settings->settings; + $search_ids = variable_get('search_api_facets_search_ids', array()); + if (count($search_ids) > 1) { + $form['global']['default_true'] = array( + '#type' => 'select', + '#title' => t('Display for searches'), + '#options' => array( + TRUE => t('For all except the selected'), + FALSE => t('Only for the selected'), + ), + '#default_value' => isset($options['default_true']) ? $options['default_true'] : TRUE, + ); + $form['global']['facet_search_ids'] = array( + '#type' => 'select', + '#title' => t('Search IDs'), + '#options' => $search_ids, + '#size' => min(4, count($search_ids)), + '#multiple' => TRUE, + '#default_value' => isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(), + ); + } + else { + $form['global']['default_true'] = array( + '#type' => 'value', + '#value' => TRUE, + ); + $form['global']['facet_search_ids'] = array( + '#type' => 'value', + '#value' => array(), + ); + } + } +} diff --git plugins/facetapi/query_type_term.inc plugins/facetapi/query_type_term.inc new file mode 100644 index 0000000..19e914b --- /dev/null +++ plugins/facetapi/query_type_term.inc @@ -0,0 +1,82 @@ +adapter->getFacet($this->facet)->getSettings(); + // Adds the operator parameter. + $operator = $settings->settings['operator']; + + // Add active facet filters. + $active = $this->adapter->getActiveItems($this->facet); + + // TODO: deal with the filters properly!! + + foreach ($active as $filter => $filter_array) { + if ($operator == '!') { + $query->condition($filter_array['field alias'], NULL); + } + else { + $filter = trim($filter_array['value'], '"'); + if (strlen($filter)) { + $query->condition($filter_array['field alias'], $filter); + } + } + } + } + + /** + * Initializes the facet's build array. + * + * @return array + * The initialized render array. + */ + public function build() { + $facet = $this->adapter->getFacet($this->facet); + // Copying the mechanism used by search_api_facets to store the current search + // per facet in a static var (during initActiveFilters) so that we can retrieve + // it here and get the correct current search for this facet. + $search_ids = drupal_static('search_api_facetapi_active_facets', array()); + if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) { + return array(); + } + $search_id = $search_ids[$facet['name']]; + $build = array(); + $search = search_api_current_search($search_id); + $results = $search[1]; + if (isset($results['search_api_facets']) && isset($results['search_api_facets']{$this->facet['field']})) { + $values = $results['search_api_facets']{$this->facet['field']}; + foreach ($values as $value) { + if ($value['count']) { + $filter = trim($value['filter'], '"'); + $build[$filter] = array('#count' => $value['count']); + } + } + } + return $build; + } +} diff --git search_api.info search_api.info index 440825b..802174a 100644 --- search_api.info +++ search_api.info @@ -21,5 +21,7 @@ files[] = includes/processor_tokenizer.inc files[] = includes/query.inc files[] = includes/server_entity.inc files[] = includes/service.inc +files[] = plugins/facetapi/adapter.inc +files[] = plugins/facetapi/query_type_term.inc configure = admin/config/search/search_api diff --git search_api.module search_api.module index 5ca8922..53fb7cc 100644 --- search_api.module +++ search_api.module @@ -149,11 +149,70 @@ function search_api_menu() { 'file' => 'search_api.admin.inc', 'type' => MENU_LOCAL_TASK, ); + // Following apachesolr module's lead here. We need to handle our own menu paths + // for facets because we need a facet configuration page per index. + if (module_exists('facetapi')) { + $file_path = drupal_get_path('module', 'facetapi'); + $first = TRUE; + foreach (facetapi_get_realm_info() as $realm_name => $realm) { + if ($first) { + $first = FALSE; + // It seems crazy to have to use our own menu callback here when ideally + // we would directly use drupal_get_form('facetapi_realm_settings_form'). + // The problem is we don't want to pass it a loaded Search API Index + // object, just the actual machine name. I'm not sure the menu system + // has a way of doing that, i.e. telling it not to replace the machine + // name with the loaded object for this particular path, but keeping it + // part of the same menu :-/ + $items[$pre . '/index/%search_api_index/facets'] = array( + 'title' => 'Facets', + 'page callback' => 'search_api_facet_settings', + 'page arguments' => array($realm_name, 5), + 'weight' => -7, + 'access arguments' => array('administer search'), + 'file path' => $file_path, + 'file' => 'facetapi.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + ); + $items[$pre . '/index/%search_api_index/facets/' . $realm_name] = array( + 'title' => $realm['label'], + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => $realm['weight'], + ); + } + else { + $items[$pre . '/index/%search_api_index/facets/' . $realm_name] = array( + 'title' => $realm['label'], + 'page callback' => 'search_api_facet_settings', + 'page arguments' => array($realm_name, 5), + 'access arguments' => array('administer search'), + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'file path' => $file_path, + 'file' => 'facetapi.admin.inc', + ); + } + } + } return $items; } /** + * Menu callback for facetapi settings page. + * + * See the explanation in the hook_menu implementation above for why this seems, + * lamentably, to be necessary. Hopefully I am just missing something painfully + * obvious :-P + */ +function search_api_facet_settings($realm_name, $search_api_index = NULL) { + return array( + 'settings_form' => drupal_get_form('facetapi_realm_settings_form', $search_api_index->machine_name, $realm_name), + ); +} + +/** * Implements hook_theme(). */ function search_api_theme() { @@ -707,6 +766,97 @@ function search_api_search_api_processor_info() { } /** + * Implements hook_facetapi_searcher_info(). + */ +function search_api_facetapi_searcher_info() { + $info = array(); + $indexes = search_api_index_load_multiple(FALSE); + foreach ($indexes as $index) { + if ($index->enabled) { + $info[$index->machine_name] = array( + 'label' => t('Search service: @name', array('@name' => $index->name)), + 'adapter' => 'search_api', + 'instance' => $index->machine_name, + 'path' => '', + ); + } + } + return $info; +} + +/** + * Implements hook_facetapi_facet_info(). + */ +function search_api_facetapi_facet_info($searcher_info) { + $facets = array(); + $index = search_api_index_load($searcher_info['instance']); + // The code below has been copied from search_api_facets_index_select (in + // search_api_facets.admin.inc) and stripped down to basics for what we need. + if (!empty($index->options['fields'])) { + $facets = array(); + if ($disabled = empty($index->enabled)) { + drupal_set_message('Since this index is at the moment disabled, no facets can be activated.', 'warning'); + } + $types = search_api_field_types(); + // TODO: figure out the rest of the callback mappings for the various field + // types. These are what take care of replacing the indexed value (such as a + // tid) with the label (e.g. the term name) for facet display. + $field_map_callbacks = array( + 'taxonomy_term' => 'facetapi_map_taxonomy_terms', + ); + foreach ($index->options['fields'] as $key => $field) { + + if (!$field['indexed']) { + continue; + } + if (empty($facets[$key])) { + $facets[$key] = array( + 'label' => t('!field', array('!field' => $field['name'])), + 'field api name' => $field['name'], + 'description' => t('Filter by field of type @type.', array('@type' => $field['name'])), + 'map callback' => (isset($field['entity_type']) && isset($field_map_callbacks[$field['entity_type']])) ? $field_map_callbacks[$field['entity_type']] : '', + ); + } + } + } + + return $facets; +} + +/** + * Implements hook_facetapi_adapters(). + */ +function search_api_facetapi_adapters() { + return array( + 'search_api' => array( + 'handler' => array( + 'class' => 'SearchApiFacetapiAdapter', + ), + ), + ); +} + +/** + * Implements hook_facetapi_query_types(). + */ +function search_api_facetapi_query_types() { + return array( + 'search_api_term' => array( + 'handler' => array( + 'class' => 'SearchApiFacetapiTerm', + 'adapter' => 'search_api', + ), + ), + //'search_api_date' => array( + // 'handler' => array( + // 'class' => 'SearchApiFacetapiDate', + // 'adapter' => 'search_api', + // ), + //), + ); +} + +/** * Mark the entities with the specified IDs as "dirty", i.e., as needing to be reindexed. * * For indexes for which items should be indexed immediately, the items are @@ -899,6 +1049,21 @@ function search_api_query($id, array $options = array()) { } /** + * Implements hook_search_api_query_alter(). + */ +function search_api_search_api_query_alter($query) { + if (module_exists('facetapi')) { + $index = $query->getIndex(); + // This is the main point of communication between the facet system and the + // search back-end - it makes the query respond to active facets. + $adapter = facetapi_adapter_load($index->machine_name); + if ($adapter) { + $adapter->addActiveFilters($query); + } + } +} + +/** * Static store for the searches executed on the current page. Can either be * used to store an executed search, or to retrieve a previously stored * search. -- 1.7.4.1