Changeset 28065


Ignore:
Timestamp:
04/03/14 22:52:38 (5 years ago)
Author:
rvelices
Message:

bug 3056: quick search -
added scoped searches tag: photo: file:
added range searches width:..1024 height: ratio: size: filesize: hits: score:

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/include/functions_search.inc.php

    r28064 r28065  
    297297define('QST_WILDCARD', QST_WILDCARD_BEGIN|QST_WILDCARD_END); 
    298298 
     299 
     300class QSearchScope 
     301{ 
     302  var $id; 
     303  var $aliases; 
     304  var $is_text; 
     305  var $allow_empty; 
     306 
     307  function __construct($id, $aliases, $allow_empty=false, $is_text=true) 
     308  { 
     309    $this->id = $id; 
     310    $this->aliases = $aliases; 
     311    $this->is_text = $is_text; 
     312    $this->allow_empty =$allow_empty; 
     313  } 
     314} 
     315 
     316class QNumericRangeScope extends QSearchScope 
     317{ 
     318  function __construct($id, $aliases, $allow_empty=false) 
     319  { 
     320    parent::__construct($id, $aliases, $allow_empty, false); 
     321  } 
     322 
     323  function parse($token) 
     324  { 
     325    $str = $token->term; 
     326    if ( ($pos = strpos($str, '..')) !== false) 
     327      $range = array( substr($str,0,$pos), substr($str, $pos+2)); 
     328    else 
     329      $range = array($str, $str); 
     330    foreach ($range as $i =>&$val) 
     331    { 
     332      if (preg_match('/^([0-9.]+)([km])?/i', $val, $matches)) 
     333      { 
     334        $val = floatval($matches[1]); 
     335        if (isset($matches[2])) 
     336        { 
     337          if ($matches[2]=='k' || $matches[2]=='K') 
     338          { 
     339            $val *= 1000; 
     340            if ($i) $val += 999; 
     341          } 
     342          if ($matches[2]=='m' || $matches[2]=='M') 
     343          { 
     344            $val *= 1000000; 
     345            if ($i) $val += 999999; 
     346          } 
     347        } 
     348      } 
     349      else 
     350        $val = ''; 
     351    } 
     352 
     353    if (!$this->allow_empty && $range[0]=='' && $range[1] == '') 
     354      return false; 
     355    $token->scope_data = $range; 
     356    return true; 
     357  } 
     358 
     359  function get_sql($field, $token) 
     360  { 
     361    $clauses = array(); 
     362    if ($token->scope_data[0]!=='') 
     363      $clauses[] = $field.' >= ' .$token->scope_data[0].' '; 
     364    if ($token->scope_data[1]!=='') 
     365      $clauses[] = $field.' <= ' .$token->scope_data[1].' '; 
     366 
     367    if (empty($clauses)) 
     368      return $field.' IS NULL'; 
     369    return '('.implode(' AND ', $clauses).')'; 
     370  } 
     371} 
     372 
    299373/** 
    300374 * Analyzes and splits the quick/query search query $q into tokens. 
     
    310384{ 
    311385  var $is_single = true; 
     386  var $modifier; 
    312387  var $term; /* the actual word/phrase string*/ 
     388  var $scope; 
     389 
     390  var $scope_data; 
    313391  var $idx; 
    314392 
    315   function __construct($term) 
     393  function __construct($term, $modifier, $scope) 
    316394  { 
    317395    $this->term = $term; 
    318   } 
    319    
     396    $this->modifier = $modifier; 
     397    $this->scope = $scope; 
     398  } 
     399 
    320400  function __toString() 
    321401  { 
    322     return $this->term; 
     402    $s = ''; 
     403    if (isset($this->scope)) 
     404      $s .= $this->scope->id .':'; 
     405    if ($this->modifier & QST_WILDCARD_BEGIN) 
     406      $s .= '*'; 
     407    if ($this->modifier & QST_QUOTED) 
     408      $s .= '"'; 
     409    $s .= $this->term; 
     410    if ($this->modifier & QST_QUOTED) 
     411      $s .= '"'; 
     412    if ($this->modifier & QST_WILDCARD_END) 
     413      $s .= '*'; 
     414    return $s; 
    323415  } 
    324416} 
     
    328420{ 
    329421  var $is_single = false; 
     422  var $modifier; 
    330423  var $tokens = array(); // the actual array of QSingleToken or QMultiToken 
    331   var $token_modifiers = array(); // modifiers (OR,NOT,...) for every token 
    332424 
    333425  function __toString() 
     
    336428    for ($i=0; $i<count($this->tokens); $i++) 
    337429    { 
    338       $modifier = $this->token_modifiers[$i]; 
     430      $modifier = $this->tokens[$i]->modifier; 
    339431      if ($i) 
    340432        $s .= ' '; 
     
    343435      if ($modifier & QST_NOT) 
    344436        $s .= 'NOT '; 
    345       if ($modifier & QST_WILDCARD_BEGIN) 
    346         $s .= '*'; 
    347       if ($modifier & QST_QUOTED) 
    348         $s .= '"'; 
    349437      if (! ($this->tokens[$i]->is_single) ) 
    350438      { 
     
    357445        $s .= $this->tokens[$i]; 
    358446      } 
    359       if ($modifier & QST_QUOTED) 
    360         $s .= '"'; 
    361       if ($modifier & QST_WILDCARD_END) 
    362         $s .= '*'; 
    363  
    364447    } 
    365448    return $s; 
    366449  } 
    367450 
    368   private function push(&$token, &$modifier) 
    369   { 
    370     $this->tokens[] = new QSingleToken($token); 
    371     $this->token_modifiers[] = $modifier; 
     451  private function push(&$token, &$modifier, &$scope) 
     452  { 
     453    if (strlen($token) || (isset($scope) && $scope->allow_empty)) 
     454    { 
     455      $this->tokens[] = new QSingleToken($token, $modifier, $scope); 
     456    } 
    372457    $token = ""; 
    373458    $modifier = 0; 
     459    $scope = null; 
    374460  } 
    375461 
     
    381467  * @param int $level the depth from root in the tree (number of opened and unclosed opening brackets) 
    382468  */ 
    383   protected function parse_expression($q, &$qi, $level) 
     469  protected function parse_expression($q, &$qi, $level, $root) 
    384470  { 
    385471    $crt_token = ""; 
    386472    $crt_modifier = 0; 
     473    $crt_scope = null; 
    387474 
    388475    for ($stop=false; !$stop && $qi<strlen($q); $qi++) 
     
    395482          case '(': 
    396483            if (strlen($crt_token)) 
    397               $this->push($crt_token, $crt_modifier); 
     484              $this->push($crt_token, $crt_modifier, $crt_scope); 
    398485            $sub = new QMultiToken; 
    399486            $qi++; 
    400             $sub->parse_expression($q, $qi, $level+1); 
     487            $sub->parse_expression($q, $qi, $level+1, $root); 
     488            $sub->modifier = $crt_modifier; 
     489            if (isset($crt_scope) && $crt_scope->is_text) 
     490            { 
     491              $sub->apply_scope($crt_scope); // eg. 'tag:(John OR Bill)' 
     492            } 
    401493            $this->tokens[] = $sub; 
    402             $this->token_modifiers[] = $crt_modifier; 
    403494            $crt_modifier = 0; 
     495            $crt_scope = null; 
    404496            break; 
    405497          case ')': 
     
    407499              $stop = true; 
    408500            break; 
     501          case ':': 
     502            $scope = @$root->scopes[$crt_token]; 
     503            if (!isset($scope) || isset($crt_scope)) 
     504            { // white space 
     505              $this->push($crt_token, $crt_modifier, $crt_scope); 
     506            } 
     507            else 
     508            { 
     509              $crt_token = ""; 
     510              $crt_scope = $scope; 
     511            } 
     512            break; 
    409513          case '"': 
    410514            if (strlen($crt_token)) 
    411               $this->push($crt_token, $crt_modifier); 
     515              $this->push($crt_token, $crt_modifier, $crt_scope); 
    412516            $crt_modifier |= QST_QUOTED; 
    413517            break; 
    414518          case '-': 
    415             if (strlen($crt_token)) 
     519            if (strlen($crt_token) || isset($crt_scope)) 
    416520              $crt_token .= $ch; 
    417521            else 
     
    424528              $crt_modifier |= QST_WILDCARD_BEGIN; 
    425529            break; 
     530          case '.': 
     531            if (isset($crt_scope) && !$crt_scope->is_text) 
     532            { 
     533              $crt_token .= $ch; 
     534              break; 
     535            } 
     536            // else white space go on.. 
    426537          default: 
    427538            if (preg_match('/[\s,.;!\?]+/', $ch)) 
    428539            { // white space 
    429540              if (strlen($crt_token)) 
    430                 $this->push($crt_token, $crt_modifier); 
     541                $this->push($crt_token, $crt_modifier, $crt_scope); 
    431542              $crt_modifier = 0; 
    432543            } 
     
    445556            $qi++; 
    446557          } 
    447           $this->push($crt_token, $crt_modifier); 
     558          $this->push($crt_token, $crt_modifier, $crt_scope); 
    448559        } 
    449560        else 
     
    452563    } 
    453564 
    454     if (strlen($crt_token)) 
    455       $this->push($crt_token, $crt_modifier); 
     565    $this->push($crt_token, $crt_modifier, $crt_scope); 
    456566 
    457567    for ($i=0; $i<count($this->tokens); $i++) 
     
    461571      if ($token->is_single) 
    462572      { 
    463         if ( ($this->token_modifiers[$i]&QST_QUOTED)==0 ) 
    464         { 
    465           if ('not' == strtolower($token->term)) 
     573        if (!isset($token->scope)) 
     574        { 
     575          if ( ($token->modifier & QST_QUOTED)==0 ) 
    466576          { 
    467             if ($i+1 < count($this->tokens)) 
    468               $this->token_modifiers[$i+1] |= QST_NOT; 
    469             $token->term = ""; 
     577            if ('not' == strtolower($token->term)) 
     578            { 
     579              if ($i+1 < count($this->tokens)) 
     580                $this->tokens[$i+1]->modifier |= QST_NOT; 
     581              $token->term = ""; 
     582            } 
     583            if ('or' == strtolower($token->term)) 
     584            { 
     585              if ($i+1 < count($this->tokens)) 
     586                $this->token[$i+1]->modifier |= QST_OR; 
     587              $token->term = ""; 
     588            } 
     589            if ('and' == strtolower($token->term)) 
     590            { 
     591              $token->term = ""; 
     592            } 
     593            if ( substr($token->term, -1)=='*' ) 
     594            { 
     595              $token->term = rtrim($token->term, '*'); 
     596              $token->modifier |= QST_WILDCARD_END; 
     597            } 
    470598          } 
    471           if ('or' == strtolower($token->term)) 
    472           { 
    473             if ($i+1 < count($this->tokens)) 
    474               $this->token_modifiers[$i+1] |= QST_OR; 
    475             $token->term = ""; 
    476           } 
    477           if ('and' == strtolower($token->term)) 
    478           { 
    479             $token->term = ""; 
    480           } 
    481           if ( substr($token->term, -1)=='*' ) 
    482           { 
    483             $token->term = rtrim($token->term, '*'); 
    484             $this->token_modifiers[$i] |= QST_WILDCARD_END; 
    485           } 
    486         } 
    487         if (!strlen($token->term)) 
    488           $remove = true; 
     599          if (!strlen($token->term)) 
     600            $remove = true; 
     601        } 
     602        elseif (!$token->scope->is_text) 
     603        { 
     604          if (!$token->scope->parse($token)) 
     605            $remove = true; 
     606        } 
    489607      } 
    490608      else 
     
    496614      { 
    497615        array_splice($this->tokens, $i, 1); 
    498         array_splice($this->token_modifiers, $i, 1); 
    499616        $i--; 
    500617      } 
     
    515632        $this->tokens[$i]->check_operator_priority(); 
    516633      if ($i==1) 
    517         $crt_prio = self::priority($this->token_modifiers[$i]); 
     634        $crt_prio = self::priority($this->tokens[$i]->modifier); 
    518635      if ($i<=1) 
    519636        continue; 
    520       $prio = self::priority($this->token_modifiers[$i]); 
     637      $prio = self::priority($this->tokens[$i]->modifier); 
    521638      if ($prio > $crt_prio) 
    522639      {// e.g. 'a OR b c d' i=2, operator(c)=AND -> prio(AND) > prio(OR) = operator(b) 
     
    524641        for ($j=$i+1; $j<count($this->tokens); $j++) 
    525642        { 
    526           if (self::priority($this->token_modifiers[$j]) >= $prio) 
     643          if (self::priority($this->tokens[$j]->modifier) >= $prio) 
    527644            $term_count++; // also take d 
    528645          else 
     
    534651        $sub = new QMultiToken; 
    535652        $sub->tokens = array_splice($this->tokens, $i, $term_count); 
    536         $sub->token_modifiers = array_splice($this->token_modifiers, $i, $term_count); 
    537653 
    538654        // rewrite ourseleves as a (b c d) 
    539655        array_splice($this->tokens, $i, 0, array($sub)); 
    540         array_splice($this->token_modifiers, $i, 0, array($sub->token_modifiers[0]&QST_OR)); 
    541         $sub->token_modifiers[0] &= ~QST_OR; 
     656        $sub->modifier = $sub->tokens[0]->modifier & QST_OR; 
     657        $sub->tokens[0]->modifier &= ~QST_OR; 
    542658 
    543659        $sub->check_operator_priority(); 
     
    551667class QExpression extends QMultiToken 
    552668{ 
     669  var $scopes = array(); 
    553670  var $stokens = array(); 
    554671  var $stoken_modifiers = array(); 
    555672 
    556   function __construct($q) 
    557   { 
     673  function __construct($q, $scopes) 
     674  { 
     675    foreach ($scopes as $scope) 
     676    { 
     677      $this->scopes[$scope->id] = $scope; 
     678      foreach ($scope->aliases as $alias) 
     679        $this->scopes[strtolower($alias)] = $scope; 
     680    } 
    558681    $i = 0; 
    559     $this->parse_expression($q, $i, 0); 
     682    $this->parse_expression($q, $i, 0, $this); 
    560683    //manipulate the tree so that 'a OR b c' is the same as 'b c OR a' 
    561684    $this->check_operator_priority(); 
     
    568691    { 
    569692      $token = $expr->tokens[$i]; 
    570       $crt_is_not = ($expr->token_modifiers[$i] ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation; 
     693      $crt_is_not = ($token->modifier ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation; 
    571694 
    572695      if ($token->is_single) 
     
    575698        $this->stokens[] = $token; 
    576699 
    577         $modifier = $expr->token_modifiers[$i]; 
     700        $modifier = $token->modifier; 
    578701        if ($crt_is_not) 
    579702          $modifier |= QST_NOT; 
     
    604727function qsearch_get_images(QExpression $expr, QResults $qsr) 
    605728{ 
    606   //@TODO: inflections for english / french 
    607729  $qsr->images_iids = array_fill(0, count($expr->tokens), array()); 
    608730 
     
    619741  for ($i=0; $i<count($expr->stokens); $i++) 
    620742  { 
    621     $token = $expr->stokens[$i]->term; 
     743    $token = $expr->stokens[$i]; 
     744    $term = $token->term; 
     745    $scope_id = isset($token->scope) ? $token->scope->id : 'photo'; 
    622746    $clauses = array(); 
    623747 
    624     $like = addslashes($token); 
     748    $like = addslashes($term); 
    625749    $like = str_replace( array('%','_'), array('\\%','\\_'), $like); // escape LIKE specials %_ 
    626     $clauses[] = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\''; 
    627  
    628     if ($inflector!=null && strlen($token)>2 
    629       && ($expr->stoken_modifiers[$i] & (QST_QUOTED|QST_WILDCARD))==0 
    630       && strcspn($token, '\'0123456789') == strlen($token) 
    631       ) 
    632     { 
    633       $variants = array_unique( array_diff( $inflector->get_variants($token), array($token) ) ); 
    634       $qsr->variants[$token] = $variants; 
    635     } 
    636     else 
    637     { 
    638       $variants = array(); 
    639     } 
    640  
    641     if (strlen($token)>3) // default minimum full text index 
    642     { 
    643       $ft = $token; 
    644       if ($expr->stoken_modifiers[$i] & QST_QUOTED) 
    645         $ft = '"'.$ft.'"'; 
    646       if ($expr->stoken_modifiers[$i] & QST_WILDCARD_END) 
    647         $ft .= '*'; 
    648       foreach ($variants as $variant) 
    649       { 
    650         $ft.=' '.$variant; 
    651       } 
    652       $clauses[] = 'MATCH(i.name, i.comment) AGAINST( \''.addslashes($ft).'\' IN BOOLEAN MODE)'; 
    653     } 
    654     else 
    655     { 
    656       foreach( array('i.name', 'i.comment') as $field) 
    657       { 
    658         /*$clauses[] = $field.' LIKE \''.$like.' %\''; 
    659         $clauses[] = $field.' LIKE \'% '.$like.'\''; 
    660         $clauses[] = $field.' LIKE \'% '.$like.' %\'';*/ 
    661         $clauses[] = $field.' REGEXP \'[[:<:]]'.addslashes(preg_quote($token)).'[[:>:]]\''; 
    662       } 
    663     } 
    664     $query = $query_base.'('.implode(' OR ', $clauses).')'; 
    665     $qsr->images_iids[$i] = query2array($query,null,'id'); 
     750    $file_like = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\''; 
     751 
     752    switch ($scope_id) 
     753    { 
     754      case 'photo': 
     755        $clauses[] = $file_like; 
     756 
     757        if ($inflector!=null && strlen($term)>2 
     758          && ($expr->stoken_modifiers[$i] & (QST_QUOTED|QST_WILDCARD))==0 
     759          && strcspn($term, '\'0123456789') == strlen($term) 
     760          ) 
     761        { 
     762          $variants = array_unique( array_diff( $inflector->get_variants($term), array($term) ) ); 
     763          $qsr->variants[$term] = $variants; 
     764        } 
     765        else 
     766        { 
     767          $variants = array(); 
     768        } 
     769 
     770        if (strlen($term)>3) // default minimum full text index 
     771        { 
     772          $ft = $term; 
     773          if ($expr->stoken_modifiers[$i] & QST_QUOTED) 
     774            $ft = '"'.$ft.'"'; 
     775          if ($expr->stoken_modifiers[$i] & QST_WILDCARD_END) 
     776            $ft .= '*'; 
     777          foreach ($variants as $variant) 
     778          { 
     779            $ft.=' '.$variant; 
     780          } 
     781          $clauses[] = 'MATCH(i.name, i.comment) AGAINST( \''.addslashes($ft).'\' IN BOOLEAN MODE)'; 
     782        } 
     783        else 
     784        { 
     785          foreach( array('i.name', 'i.comment') as $field) 
     786          { 
     787            $clauses[] = $field.' REGEXP \'[[:<:]]'.addslashes(preg_quote($term)).'[[:>:]]\''; 
     788          } 
     789        } 
     790        break; 
     791 
     792      case 'file': 
     793        $clauses[] = $file_like; 
     794        break; 
     795      case 'width': 
     796      case 'height': 
     797      case 'hits': 
     798      case 'rating_score': 
     799        $clauses[] = $token->scope->get_sql($scope_id, $token); 
     800        break; 
     801      case 'ratio': 
     802        $clauses[] = $token->scope->get_sql('width/height', $token); 
     803        break; 
     804      case 'size': 
     805        $clauses[] = $token->scope->get_sql('width*height', $token); 
     806        break; 
     807      case 'filesize': 
     808        $clauses[] = $token->scope->get_sql('filesize', $token); 
     809        break; 
     810 
     811    } 
     812    if (!empty($clauses)) 
     813    { 
     814      $query = $query_base.'('.implode(' OR ', $clauses).')'; 
     815      $qsr->images_iids[$i] = query2array($query,null,'id'); 
     816    } 
    666817  } 
    667818} 
     
    679830  foreach ($tokens as $token) 
    680831  { 
    681     $transliterated_tokens[] = transliterate($token->term); 
     832    if (!isset($token->scope) || 'tag' == $token->scope) 
     833    { 
     834      $transliterated_tokens[] = transliterate($token->term); 
     835    } 
     836    else 
     837    { 
     838      $transliterated_tokens[] = ''; 
     839    } 
    682840  } 
    683841 
     
    696854    { 
    697855      $transliterated_token = $transliterated_tokens[$i]; 
     856      if (strlen($transliterated_token)==0) 
     857        continue; 
    698858 
    699859      $match = false; 
     
    831991      $qsr->tag_iids[$i] = query2array($query, null, 'image_id'); 
    832992    } 
     993    elseif (isset($tokens[$i]->scope) && 'tag' == $tokens[$i]->scope->id && strlen($token->term)==0) 
     994    { 
     995      if ($tokens[$i]->modifier & QST_WILDCARD) 
     996      {// eg. 'tag:*' returns all tagged images 
     997        $qsr->tag_iids[$i] = query2array('SELECT DISTINCT image_id FROM '.IMAGE_TAG_TABLE, null, 'image_id'); 
     998      } 
     999      else 
     1000      {// eg. 'tag:' returns all untagged images 
     1001        $qsr->tag_iids[$i] = query2array('SELECT id FROM '.IMAGES_TABLE.' LEFT JOIN '.IMAGE_TAG_TABLE.' ON id=image_id WHERE image_id IS NULL', null, 'id'); 
     1002      } 
     1003    } 
    8331004  } 
    8341005} 
     
    8541025      $crt_ids = qsearch_eval($crt, $qsr, $crt_qualifies, $crt_ignored_terms); 
    8551026 
    856     $modifier = $expr->token_modifiers[$i]; 
     1027    $modifier = $crt->modifier; 
    8571028    if ($modifier & QST_NOT) 
    8581029      $not_ids = array_unique( array_merge($not_ids, $crt_ids)); 
     
    9111082    ); 
    9121083 
    913   $expression = new QExpression($q); 
     1084  $scopes = array(); 
     1085  $scopes[] = new QSearchScope('tag', array('tags')); 
     1086  $scopes[] = new QSearchScope('photo', array('photos')); 
     1087  $scopes[] = new QSearchScope('file', array('filename')); 
     1088  $scopes[] = new QNumericRangeScope('width', array()); 
     1089  $scopes[] = new QNumericRangeScope('height', array()); 
     1090  $scopes[] = new QNumericRangeScope('ratio', array()); 
     1091  $scopes[] = new QNumericRangeScope('size', array()); 
     1092  $scopes[] = new QNumericRangeScope('filesize', array()); 
     1093  $scopes[] = new QNumericRangeScope('hits', array('hit', 'visit', 'visits')); 
     1094  $scopes[] = new QNumericRangeScope('rating_score', array('score'), true); 
     1095  $expression = new QExpression($q, $scopes); 
    9141096//var_export($expression); 
    9151097 
Note: See TracChangeset for help on using the changeset viewer.