<?php

  
require_once('PEAR.php');
  require_once(
'DB.php');

  class 
Search
  
{
      
// {{{ properties
      
public $table;
      private 
$sql;

      
/**
      * $newFormat
      *
      * Formatting string for corrected words in Search::getAlternateSearch().
      *
      * @author Joe Stump <joe@joestump.net>
      * @access public
      * @var string $newFormat
      * @see Search::getAlternateSearch()
      */
      
public $newFormat '<i><b>%s</b></i>';

      
/**
      * $dsn
      *
      * DB DSN to connect to 
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $dsn 'mysql://root@localhost/enotes_com';

      
/**
      * $db
      *
      * DB connection handle
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $db;

      
/**
      * $pspell
      *
      * Loaded pspell dictionary
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $pspell;

      
/**
      * $lang
      *
      * Language file to load (defaults to 'en')
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $lang;

      public 
$query;
      private 
$words = array();
      private 
$mispelled = array();
      private 
$stopwords = array();

      
/**
      * $stopwordsFile
      *
      * Location of MySQL stopwords file
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $stopwordsFile '/Users/jstump/dev/search/stopwords.txt';

      
/**
      * $ignored
      *
      * List of words from the query that were not used because they were
      * stopwords (ie. "the")
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $ignored = array();

      
/**
      * $fields
      *
      * A list of FULLTEXT fields to search
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private $fields = array();

      private 
$options = array('boolean' => false,
                               
'expand'  => false);

      public 
$start 0;
      public 
$limit 20;
      
// }}}
      // {{{ __construct($lang='en') 
      
public function __construct($lang='en',$options=array())
      {
          
$this->lang $lang;

          if (
is_array($options) && count($options)) {
              
$this->options $options;
          } 

          
$this->db DB::connect($this->dsn);
          if (
PEAR::isError($this->db)) {
              throw new 
Exception($this->db->getMessage());
          } else {
              
$this->db->setFetchMode(DB_FETCHMODE_ASSOC);
          }

          
$this->pspell pspell_new($this->lang);
          if (
$this->pspell === false) {
              throw new 
Exception('Could not create pspell link');
          }

          
$fp fopen($this->stopwordsFile,'r');
          if (
is_resource($fp)) {
              while(!
feof($fp)) {
                  
$this->stopwords[] = trim(fgets($fp));
              }

              
fclose($fp);
          } else {
              throw new 
Exception('Stopwords file not found: '.$this->stopwords);
          }
      }
      
// }}}
      // {{{ setQuery($query)
      
public function setQuery($query)
      {
          
$this->query $query;
          
$this->words explode(' ',$query); 
      }
      
// }}}
      // {{{ addSearchField($name,$weight=1)
      
public function addSearchField($name,$weight)
      {
          
$this->fields[$name] = $weight;
      }
      
// }}}
      // {{{ removeStopwords()
      /**
      * removeStopwords
      *
      * Remove stopwords from the word array. Any stopwords found are stored
      * in the ignored array.
      *
      * @author Joe Stump <joe@joestump.net>
      * @access private
      */
      
private function removeStopwords()
      {
          
$final = array();
          foreach (
$this->words as $word) {
              if (!
in_array($word,$this->stopwords)) {
                  
$final[] = $word;
              } else {
                  
$this->ignored[] = $word;
              }
          }

          
$this->words $final;

          return 
count($this->ignored);
      }
      
// }}}
      
public function setOption($name,$val)
      {
          
$this->options[$name] = $val;
      }
      
// {{{ checkSpelling()
      
private function checkSpelling()
      {
          foreach (
$this->words as $word) {
              
$word eregi_replace('[^A-Z0-9\']','',$word);
              if (!
pspell_check($this->pspell,$word)) {
                  
$suggestions pspell_suggest($this->pspell,$word);
                  
$suggestion array_shift($suggestions);
                  
$this->mispelled[$word] = array('word' => $word,
                                                  
'suggestion' => $suggestion);
              }
          }

          return 
count($this->mispelled);
      }
      
// }}}
      // {{{ parseQuery()
      
private function prepare()
      {
          
$this->removeStopwords();
          
$this->checkSpelling();    

          if (!
count($this->words)) {
              return 
PEAR::raiseError('No words in search term');
          }

          if (!
count($this->fields)) {
              return 
PEAR::raiseError('No fields to search');
          } 

          
$query implode(' ',$this->words);

          if (
ereg('[^a-zA-Z0-9\'_]',$query)) {
              
// Turn on boolean searches 
              
$this->setOption('boolean',true);
              
$this->setOption('expand',false);
          } elseif (
strlen($query) < && strpos($query,' ') === false) {
              
// Turn on expansion 
              
$this->setOption('boolean',false);
              
$this->setOption('expand',true);
          }
          
          return 
$query;
      }
      
// }}}
      
public function runSearch()
      {
          
$query $this->prepare();

          
$match = array();
          foreach (
$this->fields as $name => $weight) {
              
$str "MATCH($name) AGAINST('$query'";
              if (
$this->options['boolean'] === true) {
                  
$str .= ' IN BOOLEAN MODE';
              } elseif (
$this->options['expand'] === true) {
                  
$str .= ' WITH QUERY EXPANSION';
              }

              
$str .= ')';
              
$match[] = $str;
              
$select[] = "(($str) * $weight)";
          }

          
$s implode(" + \n\t",$select);
          
$m implode(" AND \n\t",$match);

          
$sql "SELECT *,($s) AS relevancy \n".
                 
"FROM ".$this->table." \n".
                 
"WHERE ($m) \n".
                 
"ORDER BY relevancy DESC \n".
                 
"LIMIT ".$this->start.",".$this->limit;

          
$this->sql $sql;

          
$result $this->db->query($sql);
          if (!
PEAR::isError($result) && $result->numRows()) {
              
$ret = array();
              while (
$row $result->fetchRow()) {
                  
$ret[] = $row;
              }

              return 
$ret;
          }

          return array();
      }

      public function 
getSQL()
      {
          return 
$this->sql;
      }

      public function 
getMispelled()
      {
          return 
$this->mispelled;
      }

      public function 
getIgnored()
      {
          return 
$this->ignored;
      }

      public function 
getAlternateSearch()
      {
          if (
count($this->mispelled)) {
              
$alt $this->query;
              foreach (
$this->mispelled as $word) {
                  
$old $word['word'];
                  
$new sprintf($this->newFormat,$word['suggestion']);

                  
$alt str_replace($old,$new,$alt);
              }

              return 
$alt;
          }

          return 
false;
      }

      
// {{{ __destruct()
      
public function __destruct()
      {
          if (
DB::isConnection($this->db)) {
              
$this->db->disconnect();
          }
      }
      
// }}}
  
}

?>