Location: PHPKode > projects > VuFind > vufind-1.0.1/web/sys/SearchObject/Summon.php
<?php
/**
 *
 * Copyright (C) Villanova University 2010.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
require_once 'sys/Summon.php';
require_once 'sys/SearchObject/Base.php';

/* A derivative of the Search Object for use with Summon.
 */
class SearchObject_Summon extends SearchObject_Base
{
    // OTHER VARIABLES
    private $summon;        // Summon API Interface
    private $indexResult;   // Summon API Response
    
    // In the Summon configuration, facets may have extra parameters appended;
    // in most cases, we want to strip these off, but this array lets us store
    // all the extra parameters so they can be passed to the Summon class.
    private $fullFacetSettings = array();

    /**
     * Constructor. Initialise some details about the server
     *
     * @access  public
     */
    public function __construct()
    {
        global $configArray;
        
        // Standard logic from parent class:
        parent::__construct();
        
        // Set up appropriate results action:
        $this->resultsModule = 'Summon';
        $this->resultsAction = 'Search';
        
        // Set up basic and advanced Summon search types; default to basic.
        $this->searchType = $this->basicSearchType = 'Summon';
        $this->advancedSearchType = 'SummonAdvanced';
        
        // Set up facet configuration -- note that we may need to strip extra 
        // parameters from the facet names (hence the extra processing):
        $config = getExtraConfigArray('Summon');
        foreach($config['Facets'] as $key => $value) {
            $parts = explode(',', $key);
            $facetName = trim($parts[0]);
            $this->facetConfig[$facetName] = $value;
        }
        
        // Set up spelling preference
        if (isset($config['Spelling']['enabled'])) {
            $this->spellcheck    = $config['Spelling']['enabled'];
        }
        
        // Set up sort options
        $this->sortOptions = $config['Sorting'];
        if (isset($config['General']['default_sort'])) {
            $this->defaultSort = $config['General']['default_sort'];
        }
        
        // Set up search options
        $this->basicTypes = $config['Basic_Searches'];
        if (isset($config['Advanced_Searches'])) {
            $this->advancedTypes = $config['Advanced_Searches'];
        }
        
        // Set up recommendations options -- settings are found in Summon.ini:
        $this->recommendIni = 'Summon';
        
        // Connect to Summon
        $this->summon = new Summon($configArray['Summon']['apiId'], 
            $configArray['Summon']['apiKey']);
    }

    /**
     * Initialise the object from the global
     *  search parameters in $_REQUEST.
     *
     * @access  public
     * @return  boolean
     */
    public function init()
    {
        // Call the standard initialization routine in the parent:
        parent::init();

        //********************
        // Check if we have a saved search to restore -- if restored successfully,
        // our work here is done; if there is an error, we should report failure;
        // if restoreSavedSearch returns false, we should proceed as normal.
        $restored = $this->restoreSavedSearch();
        if ($restored === true) {
            return true;
        } else if (PEAR::isError($restored)) {
            return false;
        }

        $this->initView();
        $this->initPage();
        $this->initSort();
        $this->initFilters();
        
        // Try to find a basic search first; check for advanced if no basic found.
        if (!$this->initBasicSearch()) {
            $this->initAdvancedSearch();
        }
        return true;
    }
    
    /**
     * Add a field to facet on.
     *
     * @access  public
     * @param   string  $newField   Field name
     * @param   string  $newAlias   Optional on-screen display label
     */
    public function addFacet($newField, $newAlias = null)
    {
        // Save the full field name (which may include extra parameters);
        // we'll need these to do the proper search using the Summon class:
        $this->fullFacetSettings[] = $newField;
        
        // Strip parameters from field name if necessary (since they get
        // in the way of most Search Object functionality):
        $newField = explode(',', $newField);
        $newField = trim($newField[0]);
        parent::addFacet($newField, $newAlias);
    }

    /**
     * Returns the stored list of facets for the last search
     *
     * @access  public
     * @param   array   $filter         Array of field => on-screen description
     *                                  listing all of the desired facet fields;
     *                                  set to null to get all configured values.
     * @param   bool    $expandingLinks If true, we will include expanding URLs
     *                                  (i.e. get all matches for a facet, not
     *                                  just a limit to the current search) in
     *                                  the return array.
     * @return  array     Facets data arrays
     */
    public function getFacetList($filter = null, $expandingLinks = false)
    {
        // If there is no filter, we'll use all facets as the filter:
        if (is_null($filter)) {
            $filter = $this->facetConfig;
        // If there is a filter, make sure the field names are properly
        // stripped of extra parameters:
        } else {
            $oldFilter = $filter;
            $filter = array();
            foreach($oldFilter as $key => $value) {
                $key = explode(',', $key);
                $key = trim($key[0]);
                $filter[$key] = $value;
            }
        }

        // We want to sort the facets to match the order in the .ini file.  Let's
        // create a lookup array to determine order:
        $i = 0;
        $order = array();
        foreach($filter as $key => $value) {
            $order[$key] = $i++;
        }
        
        // Loop through the facets returned by Summon.
        $facetResult = array();
        if (is_array($this->indexResult['facetFields'])) {
            foreach($this->indexResult['facetFields'] as $current) {
                // The "displayName" value is actually the name of the field on
                // Summon's side -- we'll probably need to translate this to a
                // different value for actual display!
                $field = $current['displayName'];
                
                // Is this one of the fields we want to display?  If so, do work...
                if (isset($filter[$field])) {
                    // Loop through all the facet values to see if any are applied.
                    foreach($current['counts'] as $facetIndex => $facetDetails) {
                        $isApplied = false;
                        // Is this field a current filter?
                        if (in_array($field, array_keys($this->filterList))) {
                            // and is this value a selected filter?
                            if (in_array($facetDetails['value'], $this->filterList[$field])) {
                                $isApplied = true;
                            }
                        }
                        
                        // Inject "applied" value and "add filter" link into Summon results:
                        $current['counts'][$facetIndex]['isApplied'] = $isApplied;
                        $current['counts'][$facetIndex]['url'] = 
                            $this->renderLinkWithFilter("$field:".$facetDetails['value']);
                        // If we want to have expanding links (all values matching the facet)
                        // in addition to limiting links (filter current search with facet),
                        // do some extra work:
                        if ($expandingLinks) {
                            $current['counts'][$facetIndex]['expandUrl'] = 
                                $this->getExpandingFacetLink($field, $facetDetails['value']);
                        }
                    }
                    
                    // Put the current facet cluster in order based on the .ini settings,
                    // then override the display name again using .ini settings.
                    $i = $order[$field];
                    $current['label'] = $filter[$field];
                    
                    // Create a reference to counts called list for consistency with Solr
                    // output format -- this allows the facet recommendations modules to
                    // be shared between the Search and Summon modules.
                    $current['list'] = & $current['counts'];
                    $facetResult[$i] = $current;
                }
            }
        }
        ksort($facetResult);
        return $facetResult;
    }
    
    /**
     * Process spelling suggestions from the results object
     *
     * @access  private
     */
    private function processSpelling()
    {
        if (isset($this->indexResult['didYouMeanSuggestions']) &&
            is_array($this->indexResult['didYouMeanSuggestions'])) {
            foreach($this->indexResult['didYouMeanSuggestions'] as $current) {
                if (!isset($this->suggestions[$current['originalQuery']])) {
                    $this->suggestions[$current['originalQuery']] = array();
                }
                if (!isset($this->suggestions[$current['originalQuery']]['suggestions'])) {
                    $this->suggestions[$current['originalQuery']]['suggestions'] = array();
                }
                $this->suggestions[$current['originalQuery']]['suggestions'][] =
                    $current['suggestedQuery'];
            }
        }
    }

    /**
     * Actually process and submit the search
     *
     * @access  public
     * @param   bool   $returnIndexErrors  Should we die inside the index code if
     *                                     we encounter an error (false) or return
     *                                     it for access via the getIndexError() 
     *                                     method (true)?
     * @param   bool   $recommendations    Should we process recommendations along
     *                                     with the search itself?
     * @return  object Summon result structure (for now)
     */
    public function processSearch($returnIndexErrors = false, $recommendations = false)
    {
        // Build a recommendations module appropriate to the current search:
        if ($recommendations) {
            $this->initRecommendations();
        }

        // Get time before the query
        $this->startQueryTimer();

        // The "relevance" sort option is a VuFind reserved word; we need to make
        // this null in order to achieve the desired effect with Summon:
        $finalSort = ($this->sort == 'relevance') ? null : $this->sort;

        // Perform the actual search
        $this->indexResult = $this->summon->query($this->searchTerms, 
            $this->getFilterList(), $this->page, $this->limit, $finalSort, 
            $this->fullFacetSettings, $returnIndexErrors);
        if (PEAR::isError($this->indexResult)) {
            PEAR::raiseError($this->indexResult);
        }

        // Save spelling details if they exist.
        if ($this->spellcheck) {
            $this->processSpelling();
        }

        // Get time after the query
        $this->stopQueryTimer();

        // Store relevant details from the search results:
        $this->resultsTotal = $this->indexResult['recordCount'];

        // If extra processing is needed for recommendations, do it now:
        if ($recommendations && is_array($this->recommend)) {
            foreach($this->recommend as $currentSet) {
                foreach($currentSet as $current) {
                    $current->process();
                }
            }
        }

        // Send back all the details:
        return $this->indexResult;
    }

    /**
     * Get error message from index response, if any.  This will only work if 
     * processSearch was called with $returnIndexErrors set to true!
     *
     * @access  public
     * @return  mixed       false if no error, error string otherwise.
     */
    public function getIndexError()
    {
        return isset($this->indexResult['errors']) ?
            $this->indexResult['errors'] : false;
    }

    /**
     * Get database recommendations from Summon, if any.
     *
     * @access  public
     * @return  mixed       false if no recommendations, detailed array otherwise.
     */
    public function getDatabaseRecommendations()
    {
        return isset($this->indexResult['recommendationLists']['database']) ?
            $this->indexResult['recommendationLists']['database'] : false;
    }

    /**
     * Generate a URL for a basic Summon "all fields" search for a specific query.
     *
     * @access  private
     * @param   string  $lookfor        The search query.
     * @return  string                  The search URL.
     */
    private function renderBasicSummonSearch($lookfor)
    {
        // Save original settings:
        $oldType = $this->searchType;
        $oldTerms = $this->searchTerms;
        $oldPage = $this->page;

        // Create a basic search:
        $this->page = 1;
        $this->searchType = $this->basicSearchType;
        $this->searchTerms = array(array('lookfor' => $lookfor));
        $url = $this->renderSearchUrl();

        // Restore original settings:
        $this->page = $oldPage;
        $this->searchTerms = $oldTerms;
        $this->searchType = $oldType;

        // Send back generated URL:
        return $url;
    }

    /**
     * Turn the list of spelling suggestions into an array of urls
     *   for on-screen use to implement the suggestions.
     *
     * @access  public
     * @return  array     Spelling suggestion data arrays
     */
    public function getSpellingSuggestions()
    {
        $returnArray = array();
        if (count($this->suggestions) == 0) return $returnArray;

        foreach ($this->suggestions as $term => $details) {
            foreach ($details['suggestions'] as $word) {
                // Strip escaped characters in the search term (for example, "\:")
                $term = stripcslashes($term);
                $word = stripcslashes($word);
                // Basic spelling suggestion data -- since the Summon API currently
                // returns completely rewritten queries, we'll simply generate a new
                // search URL rather than trying to replace components of the
                // existing one.  This may need to become more sophisticated in the
                // future if the Summon API begins to offer suggestions in more
                // contexts.
                $returnArray[$term]['suggestions'][$word] = array(
                    'replace_url' => $this->renderBasicSummonSearch($word)
                );
            }
        }
        return $returnArray;
    }

    /**
     * Load all available facet settings.  This is mainly useful for showing
     * appropriate labels when an existing search has multiple filters associated
     * with it.
     *
     * @access  public
     * @param   string      $preferredSection       Section to favor when loading
     *                                              settings; if multiple sections
     *                                              contain the same facet, this
     *                                              section's description will be
     *                                              favored.
     */
    public function activateAllFacets($preferredSection = false)
    {
        // All Summon facets are loaded through recommendations modules; we can
        // activate the settings by starting up recommendations.  This is not a
        // very elegant solution to the problem, and we should probably revisit
        // this in the future.
        $this->initRecommendations();
    }

    /**
     * Get a user-friendly string to describe the provided facet field.
     *
     * @access  protected
     * @param   string  $field                  Facet field name.
     * @return  string                          Human-readable description of field.
     */
    protected function getFacetLabel($field)
    {
        // The default use of "Other" for undefined facets doesn't work well with
        // checkbox facets -- we'll use field names as the default within the Summon
        // search object.
        return isset($this->facetConfig[$field]) ? 
            $this->facetConfig[$field] : $field;
    }

    /**
     * Get information on the current state of the boolean checkbox facets.
     *
     * @access  public
     * @return  array
     */
    public function getCheckboxFacets()
    {
        // Grab checkbox facet details using the standard method:
        $facets = parent::getCheckboxFacets();

        // Special case -- if we have a "holdings only" facet, we want this to
        // always appear, even on the "no results" screen, since setting this
        // facet actually EXPANDS the result set, rather than reducing it:
        if (isset($facets['holdingsOnly'])) {
            $facets['holdingsOnly']['alwaysVisible'] = true;
        }

        // Return modified list:
        return $facets;
    }
}

?>
Return current item: VuFind