From d8653b43ea17b14fd1dedc7890a6c13f4dd580b8 Mon Sep 17 00:00:00 2001
From: Katherine Bailey <katherine@katbailey.net>
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 @@
+<?php
+
+/**
+ * @file
+ * Classes used by the Facet API module.
+ */
+
+/**
+ * Facet API adapter for the Search API module.
+ */
+class SearchApiFacetapiAdapter extends FacetapiAdapter {
+  /**
+   * Returns the path to the admin settings for a given realm.
+   *
+   * @param $realm_name
+   *   The name of the realm.
+   *
+   * @return
+   *   The path to the admin settings.
+   */
+  public function getPath($realm_name) {
+    $base_path = 'admin/config/search/search_api';
+    $index = $this->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 @@
+<?php
+
+/**
+ * @file
+ * Term query type plugin for the Apache Solr adapter.
+ */
+
+/**
+ * Plugin for "term" query types.
+ */
+class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTypeInterface {
+
+  /**
+   * Returns the query type associated with the plugin.
+   *
+   * @return string
+   *   The query type.
+   */
+  static public function getType() {
+    return 'term';
+  }
+
+  /**
+   * Adds the filter to the query object.
+   *
+   * @param DrupalSolrQueryInterface $query
+   *   An object containing the query in the backend's native API.
+   */
+  public function execute($query) {
+    $settings = $this->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

