From d8653b43ea17b14fd1dedc7890a6c13f4dd580b8 Mon Sep 17 00:00:00 2001 From: Katherine Bailey Date: Fri, 17 Jun 2011 14:22:12 -0700 Subject: [PATCH] Issue #1182614: initial stab at facetapi integration --- plugins/facetapi/adapter.inc | 123 +++++++++++++++++++++++++ plugins/facetapi/query_type_term.inc | 74 +++++++++++++++ search_api.info | 2 + search_api.module | 168 +++++++++++++++++++++++++++++++++- 4 files changed, 366 insertions(+), 1 deletions(-) create mode 100644 plugins/facetapi/adapter.inc create mode 100644 plugins/facetapi/query_type_term.inc diff --git a/plugins/facetapi/adapter.inc b/plugins/facetapi/adapter.inc new file mode 100644 index 0000000..eea0954 --- /dev/null +++ b/plugins/facetapi/adapter.inc @@ -0,0 +1,123 @@ +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) { + $facets = facetapi_get_enabled_facets($this->info['instance'], 'block'); + $fields = array(); + // Looked to search_api_facets_search_api_query_alter() for guidance here. + foreach ($facets as $facet) { + $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() { + // Not sure how there could be more than one current search per page request. + $searches = search_api_current_search(); + // For now, just taking the first current search :-/ + $first = key($searches); + $query = $searches[$first][0]; + $keys = $query->getKeys(); + // We need to send the keys back as a string + foreach ($keys as $key => $value) { + if ($key[0] === '#') { + unset($keys[$key]); + } + } + // TODO: what to do about the properties of the key array?? '#conjunction' + // etc...? I think this is only used for breadrumbs and things though so we + // can probably safely ignore. + return implode(' ', $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 count($searches[$first][1]); + return 0; + } + + /** + * Allows for backend specific overrides to the settings form. + */ + public function settingsForm(&$form, &$form_state) { + + } +} diff --git a/plugins/facetapi/query_type_term.inc b/plugins/facetapi/query_type_term.inc new file mode 100644 index 0000000..950a373 --- /dev/null +++ b/plugins/facetapi/query_type_term.inc @@ -0,0 +1,74 @@ +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 = substr($filter_array['value'], 1, -1); + 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() { + $build = array(); + // Again, using the "first" current search... + $searches = search_api_current_search(); + $first = key($searches); + $results = $searches[$first][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']) { + $build[$value['filter']] = array('#count' => $value['count']); + } + } + } + return $build; + } +} diff --git a/search_api.info b/search_api.info index 440825b..802174a 100644 --- a/search_api.info +++ b/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 a/search_api.module b/search_api.module index 3492bd1..3c2b4d1 100644 --- a/search_api.module +++ b/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() { @@ -703,6 +762,104 @@ function search_api_search_api_processor_info() { } /** + * Implements hook_facetapi_searcher_info(). + */ +function search_api_facetapi_searcher_info() { + $info = array(); + // Yikes, would be good to have some caching involved here, but one would hope + // that would be done by facetapi module itself..? + // We basically want to return a searcher for each Search API index, but we + // should also store the module responsible for the index (i.e. the module + // responsible for the server it's built on) in case we need to invoke hooks. + $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' => '', + ); + $server = search_api_server_load($index->server); + // Is there a better way of finding out the module responsible for this + // index? + foreach (module_implements('search_api_service_info') as $module) { + $settings = module_invoke($module, 'search_api_service_info'); + if (isset($settings[$server->class])) { + $info[$index->machine_name]['module'] = $module; + break; + } + } + } + } + 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(); + foreach ($index->options['fields'] as $key => $field) { + if (!$field['indexed']) { + continue; + } + if (empty($facets[$key])) { + $facets[$key] = array( + 'label' => t('!index: !field', array('!field' => $field['name'], '!index' => $index->name)), + 'field api name' => $field['name'], + 'description' => t('Filter by field of type @type.', array('@type' => $field['name'])), + ); + } + } + } + + 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 @@ -891,7 +1048,16 @@ function search_api_query($id, array $options = array()) { if (!$index) { throw new SearchApiException(t('Unknown index with ID !id.', array('!id' => $id))); } - return $index->query($options); + $query = $index->query($options); + if (module_exists('facetapi')) { + // 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($id); + if ($adapter) { + $adapter->addActiveFilters($query); + } + } + return $query; } /** -- 1.7.4.1