Added a new search API that allows much easier registration of search results. Basically you give the engine a table, a few columns to look at, and tell it how to format the results and you're done.
authorDan
Sat, 29 Nov 2008 00:20:57 -0500
changeset 756 e8cf18383425
parent 755 9b4cd3ef42f3
child 757 7ba7b85e1195
Added a new search API that allows much easier registration of search results. Basically you give the engine a table, a few columns to look at, and tell it how to format the results and you're done.
includes/functions.php
includes/search.php
plugins/SpecialSearch.php
--- a/includes/functions.php	Sat Nov 29 00:19:39 2008 -0500
+++ b/includes/functions.php	Sat Nov 29 00:20:57 2008 -0500
@@ -3045,6 +3045,77 @@
 }
 
 /**
+ * Registers a new type of search result. Because this is so tricky to do but keep clean, this function takes an associative array as its
+ * only parameter. This array configures the function. The required keys are:
+ *  - table: the database table to search
+ *  - titlecolumn: the column that will be used as the title of the search result. This will have a weight of 1.5
+ *  - uniqueid: a TPL-format string, variables being column names, that will be unique for every result. This should contain a string that
+ *              will be specific to your *type* of search result in addition to a primary key or other unique identifier.
+ *  - linkformat: an array with the keys page_id and namespace which are where your result will link, plus the following additional options:
+ *     - append: added to the full generated URL
+ *     - query: query string without initial "?"
+ * Additional options:
+ *  - datacolumn
+ *  - additionalcolumns: additional data to select if you want to use a custom formatting callback
+ *  - formatcallback: a callback or TPL string. If a callback, it will be called with the parameters being the current result row and an
+ *                    array of words in case you want to highlight anything; the callback will be expected to return a string containing
+ *                    a fully formatted and sanitized blob of HTML. If formatcallback is a TPL string, variables will be named after table
+ *                    columns.
+ *  - additionalwhere: additional SQL to inject into WHERE clause, in the format of "AND foo = bar"
+ * @example Working example of adding users to search results:
+ <code>
+  register_search_handler(array(
+      'table' => 'users',
+      'titlecolumn' => 'username',
+      'uniqueid' => 'ns=User;cid={username}',
+      'additionalcolumns' => array('user_id'),
+      'resultnote' => '[Member]',
+      'linkformat' => array(
+          'page_id' => '{username}',
+          'namespace' => 'User'
+        ),
+      'formatcallback' => 'format_user_search_result',
+    ));
+  
+  function format_user_search_result($row)
+  {
+    global $session, $lang;
+    $rankdata = $session->get_user_rank(intval($row['user_id']));
+    $rankspan = '<span style="' . $rankdata['rank_style'] . '">' . $lang->get($rankdata['rank_title']) . '</span>';
+    if ( empty($rankdata['user_title']) )
+    {
+      return $rankspan;
+    }
+    else
+    {
+      return '"' . htmlspecialchars($rankdata['user_title']) . "\" (<b>$rankspan</b>)";
+    }
+  }
+ </code>
+ * @param array Options array - see function documentation
+ * @return null
+ */
+
+global $search_handlers;
+$search_handlers = array();
+
+function register_search_handler($options)
+{
+  global $search_handlers;
+  
+  $required = array('table', 'titlecolumn', 'uniqueid', 'linkformat');
+  foreach ( $required as $key )
+  {
+    if ( !isset($options[$key]) )
+    {
+      throw new Exception("Required search handler option '$key' is missing");
+    }
+  }
+  $search_handlers[] = $options;
+  return null;
+}
+
+/**
  * From http://us2.php.net/urldecode - decode %uXXXX
  * @param string The urlencoded string
  * @return string
--- a/includes/search.php	Sat Nov 29 00:19:39 2008 -0500
+++ b/includes/search.php	Sat Nov 29 00:20:57 2008 -0500
@@ -105,6 +105,12 @@
   global $lang;
   
   $warnings = array();
+  
+  //
+  // STAGE 0: PARSE SEARCH QUERY
+  // Identify all terms of the query. Separate between what is required and what is not, and what should be sent through the index as
+  // opposed to straight-out LIKE-selected.
+  //
 
   $query = parse_search_query($query, $warnings);
 
@@ -538,6 +544,8 @@
   // pages and add text, etc. as necessary.
   // Plugins are COMPLETELY responsible for using the search terms and handling Boolean logic properly
 
+  inject_custom_search_results($query, $query_phrase, $scores, $page_data, $case_sensitive, $word_list);
+  
   $code = $plugins->setHook('search_global_inner');
   foreach ( $code as $cmd )
   {
@@ -922,4 +930,186 @@
   return $stopwords;
 }
 
+/**
+ * Private function to inject custom results into a search.
+ */
+
+function inject_custom_search_results(&$query, &$query_phrase, &$scores, &$page_data, &$case_sensitive, &$word_list)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
+  global $search_handlers;
+  
+  // global functions
+  $terms = array(
+      'any' => array_merge($query['any'], $query_phrase['any']),
+      'req' => array_merge($query['req'], $query_phrase['req']),
+      'not' => $query['not']
+    );
+  
+  foreach ( $search_handlers as &$options )
+  {
+    $where = array('any' => array(), 'req' => array(), 'not' => array());
+    $where_any =& $where['any'];
+    $where_req =& $where['req'];
+    $where_not =& $where['not'];
+    $title_col = ( $case_sensitive ) ? $options['titlecolumn'] : 'lcase(' . $options['titlecolumn'] . ')';
+    if ( isset($options['datacolumn']) )
+      $desc_col = ( $case_sensitive ) ? $options['datacolumn'] : 'lcase(' . $options['datacolumn'] . ')';
+    else
+      $desc_col = "''";
+    foreach ( $terms['any'] as $term )
+    {
+      $term = escape_string_like($term);
+      if ( !$case_sensitive )
+        $term = strtolower($term);
+      $where_any[] = "( $title_col LIKE '%{$term}%' OR $desc_col LIKE '%{$term}%' )";
+    }
+    foreach ( $terms['req'] as $term )
+    {
+      $term = escape_string_like($term);
+      if ( !$case_sensitive )
+        $term = strtolower($term);
+      $where_req[] = "( $title_col LIKE '%{$term}%' OR $desc_col LIKE '%{$term}%' )";
+    }
+    foreach ( $terms['not'] as $term )
+    {
+      $term = escape_string_like($term);
+      if ( !$case_sensitive )
+        $term = strtolower($term);
+      $where_not[] = "$title_col NOT LIKE '%{$term}%' AND $desc_col NOT LIKE '%{$term}%'";
+    }
+    if ( empty($where_any) )
+      unset($where_any, $where['any']);
+    if ( empty($where_req) )
+      unset($where_req, $where['req']);
+    if ( empty($where_not) )
+      unset($where_not, $where['not']);
+    
+    $where_any = '(' . implode(' OR ', $where_any) . '' . ( isset($where['req']) || isset($where['not']) ? ' OR 1 = 1' : '' ) . ')';
+    
+    if ( isset($where_req) )
+      $where_req = implode(' AND ', $where_req);
+    if ( isset($where_not) )
+    $where_not = implode( 'AND ', $where_not);
+    
+    $where = implode(' AND ', $where);
+    
+    $columns = $options['titlecolumn'];
+    if ( isset($options['datacolumn']) )
+      $columns .= ", {$options['datacolumn']}";
+    if ( isset($options['additionalcolumns']) )
+      $columns .= ', ' . implode(', ', $options['additionalcolumns']);
+    
+    $sql = "SELECT $columns FROM " . table_prefix . "{$options['table']} WHERE ( $where ) $additionalwhere;";
+  
+    if ( !($q = $db->sql_unbuffered_query($sql)) )
+    {
+      $db->_die('Automatically generated search query');
+    }
+    
+    if ( $row = $db->fetchrow() )
+    {
+      do
+      {
+        $parser = $template->makeParserText($options['uniqueid']);
+        $parser->assign_vars($row);
+        $idstring = $parser->run();
+        
+        // Score this result
+        foreach ( $word_list as $term )
+        {
+          if ( $case_sensitive )
+          {
+            if ( strstr($row[$options['titlecolumn']], $term) )
+            {
+              ( isset($scores[$idstring]) ) ? $scores[$idstring] += 1.5 : $scores[$idstring] = 1.5;
+            }
+            else if ( isset($options['datacolumn']) && strstr($row[$options['datacolumn']], $term) )
+            {
+              ( isset($scores[$idstring]) ) ? $scores[$idstring]++ : $scores[$idstring] = 1;
+            }
+          }
+          else
+          {
+            if ( stristr($row[$options['titlecolumn']], $term) )
+            {
+              ( isset($scores[$idstring]) ) ? $scores[$idstring] += 1.5 : $scores[$idstring] = 1.5;
+            }
+            else if ( isset($options['datacolumn']) && stristr($row[$options['datacolumn']], $term) )
+            {
+              ( isset($scores[$idstring]) ) ? $scores[$idstring]++ : $scores[$idstring] = 1;
+            }
+          }
+        }
+        // Generate text...
+        $text = '';
+        if ( isset($options['datacolumn']) && !isset($options['formatcallback']) )
+        {
+          $text = highlight_and_clip_search_result(htmlspecialchars($row[$options['datacolumn']]), $word_list);
+        }
+        else if ( isset($options['formatcallback']) )
+        {
+          if ( is_callable($options['formatcallback']) )
+          {
+            $text = @call_user_func($options['formatcallback'], $row, $word_list);
+          }
+          else
+          {
+            $parser = $template->makeParserText($options['formatcallback']);
+            $parser->assign_vars($row);
+            $text = $parser->run();
+          }
+        }
+        
+        // Inject result
+        
+        if ( isset($scores[$idstring]) )
+        {
+          $parser = $template->makeParserText($options['linkformat']['page_id']);
+          $parser->assign_vars($row);
+          $page_id = $parser->run();
+          
+          $parser = $template->makeParserText($options['linkformat']['namespace']);
+          $parser->assign_vars($row);
+          $namespace = $parser->run();
+          
+          $page_data[$idstring] = array(
+            'page_name' => highlight_search_result(htmlspecialchars($row[$options['titlecolumn']]), $word_list),
+            'page_text' => $text,
+            'score' => $scores[$idstring],
+            'page_id' => $page_id,
+            'namespace' => $namespace,
+          );
+          
+          // Any additional flags that need to be added to the result?
+          // The small usually-bracketed text to the left of the title
+          if ( isset($options['resultnote']) )
+          {
+            $page_data[$idstring]['page_note'] = $options['resultnote'];
+          }
+          // Should we include the length?
+          if ( isset($options['datacolumn']) )
+          {
+            $page_data[$idstring]['page_length'] = strlen($row[$options['datacolumn']]);
+          }
+          else
+          {
+            $page_data[$idstring]['page_length'] = 0;
+            $page_data[$idstring]['zero_length'] = true;
+          }
+          // Anything to append to result links?
+          if ( isset($options['linkformat']['append']) )
+          {
+            $page_data[$idstring]['url_append'] = $options['linkformat']['append'];
+          }
+        }
+      }
+      while ( $row = $db->fetchrow($q) );
+      $db->free_result($q);
+    }
+  }
+}
+
 ?>
--- a/plugins/SpecialSearch.php	Sat Nov 29 00:19:39 2008 -0500
+++ b/plugins/SpecialSearch.php	Sat Nov 29 00:20:57 2008 -0500
@@ -175,12 +175,12 @@
          'PAGE_TEXT' => $result['page_text'],
          'PAGE_LENGTH' => $result['page_length'],
          'RELEVANCE_SCORE' => $result['score'],
-         'RESULT_URL' => makeUrlNS($result['namespace'], $result['page_id'], false, true),
+         'RESULT_URL' => makeUrlNS($result['namespace'], $result['page_id'], false, true) . ( isset($result['url_append']) ? $result['url_append'] : '' ),
          'PAGE_LENGTH_UNIT' => $length_unit,
          'PAGE_URL' => $url,
          'PAGE_NOTE' => ( isset($result['page_note']) ? $result['page_note'] . ' ' : '' )
         ));
-      $has_content = ( $result['namespace'] == 'Special' );
+      $has_content = ( $result['namespace'] == 'Special' || !empty($result['zero_length']) );
       
       $code = $plugins->setHook('search_global_results');
       foreach ( $code as $cmd )