source: trunk/include/functions_search.inc.php @ 27882

Revision 27882, 26.3 KB checked in by rvelices, 6 years ago (diff)

bug 3056: quick search OR operator priority taken into account
search for 'mary qwerty' will ignore 'qwerty' and return only results for 'mary' if there is no such thing as 'qwerty' in the photos (if there was 'mary' and 'qwerty', the results for both 'mary' AND 'qwerty' would be shown)

  • Property svn:eol-style set to LF
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2014 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24/**
25 * @package functions\search
26 */
27
28
29/**
30 * Returns search rules stored into a serialized array in "search"
31 * table. Each search rules set is numericaly identified.
32 *
33 * @param int $search_id
34 * @return array
35 */
36function get_search_array($search_id)
37{
38  if (!is_numeric($search_id))
39  {
40    die('Search id must be an integer');
41  }
42
43  $query = '
44SELECT rules
45  FROM '.SEARCH_TABLE.'
46  WHERE id = '.$search_id.'
47;';
48  list($serialized_rules) = pwg_db_fetch_row(pwg_query($query));
49
50  return unserialize($serialized_rules);
51}
52
53/**
54 * Returns the SQL clause for a search.
55 * Transforms the array returned by get_search_array() into SQL sub-query.
56 *
57 * @param array $search
58 * @return string
59 */
60function get_sql_search_clause($search)
61{
62  // SQL where clauses are stored in $clauses array during query
63  // construction
64  $clauses = array();
65
66  foreach (array('file','name','comment','author') as $textfield)
67  {
68    if (isset($search['fields'][$textfield]))
69    {
70      $local_clauses = array();
71      foreach ($search['fields'][$textfield]['words'] as $word)
72      {
73        $local_clauses[] = $textfield." LIKE '%".$word."%'";
74      }
75
76      // adds brackets around where clauses
77      $local_clauses = prepend_append_array_items($local_clauses, '(', ')');
78
79      $clauses[] = implode(
80        ' '.$search['fields'][$textfield]['mode'].' ',
81        $local_clauses
82        );
83    }
84  }
85
86  if (isset($search['fields']['allwords']))
87  {
88    $fields = array('file', 'name', 'comment', 'author');
89    // in the OR mode, request bust be :
90    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
91    // OR (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
92    //
93    // in the AND mode :
94    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
95    // AND (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
96    $word_clauses = array();
97    foreach ($search['fields']['allwords']['words'] as $word)
98    {
99      $field_clauses = array();
100      foreach ($fields as $field)
101      {
102        $field_clauses[] = $field." LIKE '%".$word."%'";
103      }
104      // adds brackets around where clauses
105      $word_clauses[] = implode(
106        "\n          OR ",
107        $field_clauses
108        );
109    }
110
111    array_walk(
112      $word_clauses,
113      create_function('&$s','$s="(".$s.")";')
114      );
115
116    // make sure the "mode" is either OR or AND
117    if ($search['fields']['allwords']['mode'] != 'AND' and $search['fields']['allwords']['mode'] != 'OR')
118    {
119      $search['fields']['allwords']['mode'] = 'AND';
120    }
121
122    $clauses[] = "\n         ".
123      implode(
124        "\n         ". $search['fields']['allwords']['mode']. "\n         ",
125        $word_clauses
126        );
127  }
128
129  foreach (array('date_available', 'date_creation') as $datefield)
130  {
131    if (isset($search['fields'][$datefield]))
132    {
133      $clauses[] = $datefield." = '".$search['fields'][$datefield]['date']."'";
134    }
135
136    foreach (array('after','before') as $suffix)
137    {
138      $key = $datefield.'-'.$suffix;
139
140      if (isset($search['fields'][$key]))
141      {
142        $clauses[] = $datefield.
143          ($suffix == 'after'             ? ' >' : ' <').
144          ($search['fields'][$key]['inc'] ? '='  : '').
145          " '".$search['fields'][$key]['date']."'";
146      }
147    }
148  }
149
150  if (isset($search['fields']['cat']))
151  {
152    if ($search['fields']['cat']['sub_inc'])
153    {
154      // searching all the categories id of sub-categories
155      $cat_ids = get_subcat_ids($search['fields']['cat']['words']);
156    }
157    else
158    {
159      $cat_ids = $search['fields']['cat']['words'];
160    }
161
162    $local_clause = 'category_id IN ('.implode(',', $cat_ids).')';
163    $clauses[] = $local_clause;
164  }
165
166  // adds brackets around where clauses
167  $clauses = prepend_append_array_items($clauses, '(', ')');
168
169  $where_separator =
170    implode(
171      "\n    ".$search['mode'].' ',
172      $clauses
173      );
174
175  $search_clause = $where_separator;
176
177  return $search_clause;
178}
179
180/**
181 * Returns the list of items corresponding to the advanced search array.
182 *
183 * @param array $search
184 * @param string $images_where optional additional restriction on images table
185 * @return array
186 */
187function get_regular_search_results($search, $images_where='')
188{
189  global $conf;
190  $forbidden = get_sql_condition_FandF(
191        array
192          (
193            'forbidden_categories' => 'category_id',
194            'visible_categories' => 'category_id',
195            'visible_images' => 'id'
196          ),
197        "\n  AND"
198    );
199
200  $items = array();
201  $tag_items = array();
202
203  if (isset($search['fields']['tags']))
204  {
205    $tag_items = get_image_ids_for_tags(
206      $search['fields']['tags']['words'],
207      $search['fields']['tags']['mode']
208      );
209  }
210
211  $search_clause = get_sql_search_clause($search);
212
213  if (!empty($search_clause))
214  {
215    $query = '
216SELECT DISTINCT(id)
217  FROM '.IMAGES_TABLE.' i
218    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
219  WHERE '.$search_clause;
220    if (!empty($images_where))
221    {
222      $query .= "\n  AND ".$images_where;
223    }
224    $query .= $forbidden.'
225  '.$conf['order_by'];
226    $items = array_from_query($query, 'id');
227  }
228
229  if ( !empty($tag_items) )
230  {
231    switch ($search['mode'])
232    {
233      case 'AND':
234        if (empty($search_clause))
235        {
236          $items = $tag_items;
237        }
238        else
239        {
240          $items = array_values( array_intersect($items, $tag_items) );
241        }
242        break;
243      case 'OR':
244        $before_count = count($items);
245        $items = array_unique(
246          array_merge(
247            $items,
248            $tag_items
249            )
250          );
251        break;
252    }
253  }
254
255  return $items;
256}
257
258/**
259 * Finds if a char is a letter, a figure or any char of the extended ASCII table (>127).
260 *
261 * @param char $ch
262 * @return bool
263 */
264function is_word_char($ch)
265{
266  return ($ch>='0' && $ch<='9') || ($ch>='a' && $ch<='z') || ($ch>='A' && $ch<='Z') || ord($ch)>127;
267}
268
269/**
270 * Finds if a char is a special token for word start: [{<=*+
271 *
272 * @param char $ch
273 * @return bool
274 */
275function is_odd_wbreak_begin($ch)
276{
277  return strpos('[{<=*+', $ch)===false ? false:true;
278}
279
280/**
281 * Finds if a char is a special token for word end: ]}>=*+
282 *
283 * @param char $ch
284 * @return bool
285 */
286function is_odd_wbreak_end($ch)
287{
288  return strpos(']}>=*+', $ch)===false ? false:true;
289}
290
291
292define('QST_QUOTED',         0x01);
293define('QST_NOT',            0x02);
294define('QST_OR',             0x04);
295define('QST_WILDCARD_BEGIN', 0x08);
296define('QST_WILDCARD_END',   0x10);
297define('QST_WILDCARD', QST_WILDCARD_BEGIN|QST_WILDCARD_END);
298
299/**
300 * Analyzes and splits the quick/query search query $q into tokens.
301 * q='john bill' => 2 tokens 'john' 'bill'
302 * Special characters for MySql full text search (+,<,>,~) appear in the token modifiers.
303 * The query can contain a phrase: 'Pierre "New York"' will return 'pierre' qnd 'new york'.
304 *
305 * @param string $q
306 */
307
308/** Represents a single word or quoted phrase to be searched.*/
309class QSingleToken
310{
311  var $is_single = true;
312  var $token; /* the actual word/phrase string*/
313  var $idx;
314
315  function __construct($token)
316  {
317    $this->token = $token;
318  }
319}
320
321/** Represents an expression of several words or sub expressions to be searched.*/
322class QMultiToken
323{
324  var $is_single = false;
325  var $tokens = array(); // the actual array of QSingleToken or QMultiToken
326  var $token_modifiers = array(); // modifiers (OR,NOT,...) for every token
327
328  function __toString()
329  {
330    $s = '';
331    for ($i=0; $i<count($this->tokens); $i++)
332    {
333      $modifier = $this->token_modifiers[$i];
334      if ($i)
335        $s .= ' ';
336      if ($modifier & QST_OR)
337        $s .= 'OR ';
338      if ($modifier & QST_NOT)
339        $s .= 'NOT ';
340      if ($modifier & QST_WILDCARD_BEGIN)
341        $s .= '*';
342      if ($modifier & QST_QUOTED)
343        $s .= '"';
344      if (! ($this->tokens[$i]->is_single) )
345      {
346        $s .= '(';
347        $s .= $this->tokens[$i];
348        $s .= ')';
349      }
350      else
351      {
352        $s .= $this->tokens[$i]->token;
353      }
354      if ($modifier & QST_QUOTED)
355        $s .= '"';
356      if ($modifier & QST_WILDCARD_END)
357        $s .= '*';
358
359    }
360    return $s;
361  }
362
363  private function push(&$token, &$modifier)
364  {
365    $this->tokens[] = new QSingleToken($token);
366    $this->token_modifiers[] = $modifier;
367    $token = "";
368    $modifier = 0;
369  }
370
371  /**
372  * Parses the input query string by tokenizing the input, generating the modifiers (and/or/not/quotation/wildcards...).
373  * Recursivity occurs when parsing ()
374  * @param string $q the actual query to be parsed
375  * @param int $qi the character index in $q where to start parsing
376  * @param int $level the depth from root in the tree (number of opened and unclosed opening brackets)
377  */
378  protected function parse_expression($q, &$qi, $level)
379  {
380    $crt_token = "";
381    $crt_modifier = 0;
382
383    for ($stop=false; !$stop && $qi<strlen($q); $qi++)
384    {
385      $ch = $q[$qi];
386      if ( ($crt_modifier&QST_QUOTED)==0)
387      {
388        switch ($ch)
389        {
390          case '(':
391            if (strlen($crt_token))
392              $this->push($crt_token, $crt_modifier);
393            $sub = new QMultiToken;
394            $qi++;
395            $sub->parse_expression($q, $qi, $level+1);
396            $this->tokens[] = $sub;
397            $this->token_modifiers[] = $crt_modifier;
398            $crt_modifier = 0;
399            break;
400          case ')':
401            if ($level>0)
402              $stop = true;
403            break;
404          case '"':
405            if (strlen($crt_token))
406              $this->push($crt_token, $crt_modifier);
407            $crt_modifier |= QST_QUOTED;
408            break;
409          case '-':
410            if (strlen($crt_token))
411              $crt_token .= $ch;
412            else
413              $crt_modifier |= QST_NOT;
414            break;
415          case '*':
416            if (strlen($crt_token))
417              $crt_token .= $ch; // wildcard end later
418            else
419              $crt_modifier |= QST_WILDCARD_BEGIN;
420            break;
421          default:
422            if (preg_match('/[\s,.;!\?]+/', $ch))
423            { // white space
424              if (strlen($crt_token))
425                $this->push($crt_token, $crt_modifier);
426              $crt_modifier = 0;
427            }
428            else
429              $crt_token .= $ch;
430            break;
431        }
432      }
433      else
434      {// quoted
435        if ($ch=='"')
436        {
437          if ($qi+1 < strlen($q) && $q[$qi+1]=='*')
438          {
439            $crt_modifier |= QST_WILDCARD_END;
440            $ai++;
441          }
442          $this->push($crt_token, $crt_modifier);
443        }
444        else
445          $crt_token .= $ch;
446      }
447    }
448
449    if (strlen($crt_token))
450      $this->push($crt_token, $crt_modifier);
451
452    for ($i=0; $i<count($this->tokens); $i++)
453    {
454      $token = $this->tokens[$i];
455      $remove = false;
456      if ($token->is_single)
457      {
458        if ( ($this->token_modifiers[$i]&QST_QUOTED)==0 )
459        {
460          if ('not' == strtolower($token->token))
461          {
462            if ($i+1 < count($this->tokens))
463              $this->token_modifiers[$i+1] |= QST_NOT;
464            $token->token = "";
465          }
466          if ('or' == strtolower($token->token))
467          {
468            if ($i+1 < count($this->tokens))
469              $this->token_modifiers[$i+1] |= QST_OR;
470            $token->token = "";
471          }
472          if ('and' == strtolower($token->token))
473          {
474            $token->token = "";
475          }
476          if ( substr($token->token, -1)=='*' )
477          {
478            $token->token = rtrim($token->token, '*');
479            $this->token_modifiers[$i] |= QST_WILDCARD_END;
480          }
481        }
482        if (!strlen($token->token))
483          $remove = true;
484      }
485      else
486      {
487        if (!count($token->tokens))
488          $remove = true;
489      }
490      if ($remove)
491      {
492        array_splice($this->tokens, $i, 1);
493        array_splice($this->token_modifiers, $i, 1);
494        $i--;
495      }
496    }
497  }
498
499  private static function priority($modifier)
500  {
501    return $modifier & QST_OR ? 0 :1;
502  }
503
504  /* because evaluations occur left to right, we ensure that 'a OR b c d' is interpreted as 'a OR (b c d)'*/
505  protected function check_operator_priority()
506  {
507    for ($i=0; $i<count($this->tokens); $i++)
508    {
509      if (!$this->tokens[$i]->is_single)
510        $this->tokens[$i]->check_operator_priority();
511      if ($i==1)
512        $crt_prio = self::priority($this->token_modifiers[$i]);
513      if ($i<=1)
514        continue;
515      $prio = self::priority($this->token_modifiers[$i]);
516      if ($prio > $crt_prio)
517      {// e.g. 'a OR b c d' i=2, operator(c)=AND -> prio(AND) > prio(OR) = operator(b)
518        $term_count = 2; // at least b and c to be regrouped
519        for ($j=$i+1; $j<count($this->tokens); $j++)
520        {
521          if (self::priority($this->token_modifiers[$j]) >= $prio)
522            $term_count++; // also take d
523          else
524            break;
525        }
526
527        $i--; // move pointer to b
528        // crate sub expression (b c d)
529        $sub = new QMultiToken;
530        $sub->tokens = array_splice($this->tokens, $i, $term_count);
531        $sub->token_modifiers = array_splice($this->token_modifiers, $i, $term_count);
532
533        // rewrite ourseleves as a (b c d)
534        array_splice($this->tokens, $i, 0, array($sub));
535        array_splice($this->token_modifiers, $i, 0, array($sub->token_modifiers[0]&QST_OR));
536        $sub->token_modifiers[0] &= ~QST_OR;
537
538        $sub->check_operator_priority();
539      }
540      else
541        $crt_prio = $prio;
542    }
543  }
544}
545
546class QExpression extends QMultiToken
547{
548  var $stokens = array();
549  var $stoken_modifiers = array();
550
551  function __construct($q)
552  {
553    $i = 0;
554    $this->parse_expression($q, $i, 0);
555    //manipulate the tree so that 'a OR b c' is the same as 'b c OR a'
556    $this->check_operator_priority();
557    $this->build_single_tokens($this, 0);
558  }
559
560  private function build_single_tokens(QMultiToken $expr, $this_is_not)
561  {
562    for ($i=0; $i<count($expr->tokens); $i++)
563    {
564      $token = $expr->tokens[$i];
565      $crt_is_not = ($expr->token_modifiers[$i] ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation;
566
567      if ($token->is_single)
568      {
569        $token->idx = count($this->stokens);
570        $this->stokens[] = $token->token;
571
572        $modifier = $expr->token_modifiers[$i];
573        if ($crt_is_not)
574          $modifier |= QST_NOT;
575        else
576          $modifier &= ~QST_NOT;
577        $this->stoken_modifiers[] = $modifier;
578      }
579      else
580        $this->build_single_tokens($token, $crt_is_not);
581    }
582  }
583}
584
585/**
586  Structure of results being filled from different tables
587*/
588class QResults
589{
590  var $all_tags;
591  var $tag_ids;
592  var $tag_iids;
593  var $images_iids;
594  var $iids;
595}
596
597function qsearch_get_images(QExpression $expr, QResults $qsr)
598{
599  //@TODO: inflections for english / french
600  $qsr->images_iids = array_fill(0, count($expr->tokens), array());
601  $query_base = 'SELECT id from '.IMAGES_TABLE.' i WHERE ';
602  for ($i=0; $i<count($expr->stokens); $i++)
603  {
604    $token = $expr->stokens[$i];
605    $clauses = array();
606
607    $like = addslashes($token);
608    $like = str_replace( array('%','_'), array('\\%','\\_'), $like); // escape LIKE specials %_
609    $clauses[] = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\'';
610
611    if (strlen($token)>3) // default minimum full text index
612    {
613      $ft = $token;
614      if ($expr->stoken_modifiers[$i] & QST_QUOTED)
615        $ft = '"'.$ft.'"';
616      if ($expr->stoken_modifiers[$i] & QST_WILDCARD_END)
617        $ft .= '*';
618      $clauses[] = 'MATCH(i.name, i.comment) AGAINST( \''.addslashes($ft).'\' IN BOOLEAN MODE)';
619    }
620    else
621    {
622      foreach( array('i.name', 'i.comment') as $field)
623      {
624        $clauses[] = $field.' LIKE \''.$like.' %\'';
625        $clauses[] = $field.' LIKE \'% '.$like.'\'';
626        $clauses[] = $field.' LIKE \'% '.$like.' %\'';
627      }
628    }
629    $query = $query_base.'('.implode(' OR ', $clauses).')';
630    $qsr->images_iids[$i] = query2array($query,null,'id');
631  }
632}
633
634function qsearch_get_tags(QExpression $expr, QResults $qsr)
635{
636  $tokens = $expr->stokens;
637  $token_modifiers = $expr->stoken_modifiers;
638
639  $token_tag_ids = array_fill(0, count($tokens), array() );
640  $all_tags = array();
641
642  $token_tag_scores = $token_tag_ids;
643  $transliterated_tokens = array();
644  foreach ($tokens as $token)
645  {
646    $transliterated_tokens[] = transliterate($token);
647  }
648
649  $query = '
650SELECT t.*, COUNT(image_id) AS counter
651  FROM '.TAGS_TABLE.' t
652    INNER JOIN '.IMAGE_TAG_TABLE.' ON id=tag_id
653  GROUP BY id';
654  $result = pwg_query($query);
655  while ($tag = pwg_db_fetch_assoc($result))
656  {
657    $transliterated_tag = transliterate($tag['name']);
658
659    // find how this tag matches query tokens
660    for ($i=0; $i<count($tokens); $i++)
661    {
662      $transliterated_token = $transliterated_tokens[$i];
663
664      $match = false;
665      $pos = 0;
666      while ( ($pos = strpos($transliterated_tag, $transliterated_token, $pos)) !== false)
667      {
668        if ( ($token_modifiers[$i]&QST_WILDCARD)==QST_WILDCARD )
669        {// wildcard in this token
670          $match = 1;
671          break;
672        }
673        $token_len = strlen($transliterated_token);
674
675        // search begin of word
676        $wbegin_len=0; $wbegin_char=' ';
677        while ($pos-$wbegin_len > 0)
678        {
679          if (! is_word_char($transliterated_tag[$pos-$wbegin_len-1]) )
680          {
681            $wbegin_char = $transliterated_tag[$pos-$wbegin_len-1];
682            break;
683          }
684          $wbegin_len++;
685        }
686
687        // search end of word
688        $wend_len=0; $wend_char=' ';
689        while ($pos+$token_len+$wend_len < strlen($transliterated_tag))
690        {
691          if (! is_word_char($transliterated_tag[$pos+$token_len+$wend_len]) )
692          {
693            $wend_char = $transliterated_tag[$pos+$token_len+$wend_len];
694            break;
695          }
696          $wend_len++;
697        }
698
699        $this_score = 0;
700        if ( ($token_modifiers[$i]&QST_WILDCARD)==0 )
701        {// no wildcard begin or end
702          if ($token_len <= 2)
703          {// search for 1 or 2 characters must match exactly to avoid retrieving too much data
704            if ($wbegin_len==0 && $wend_len==0 && !is_odd_wbreak_begin($wbegin_char) && !is_odd_wbreak_end($wend_char) )
705              $this_score = 1;
706          }
707          elseif ($token_len == 3)
708          {
709            if ($wbegin_len==0)
710              $this_score = $token_len / ($token_len + $wend_len);
711          }
712          else
713          {
714            $this_score = $token_len / ($token_len + 1.1 * $wbegin_len + 0.9 * $wend_len);
715          }
716        }
717
718        if ($this_score>0)
719          $match = max($match, $this_score );
720        $pos++;
721      }
722
723      if ($match)
724      {
725        $tag_id = (int)$tag['id'];
726        $all_tags[$tag_id] = $tag;
727        $token_tag_ids[$i][] = $tag_id;
728        $token_tag_scores[$i][] = $match;
729      }
730    }
731  }
732
733  // process tags
734  $not_tag_ids = array();
735  for ($i=0; $i<count($tokens); $i++)
736  {
737    array_multisort($token_tag_scores[$i], SORT_DESC|SORT_NUMERIC, $token_tag_ids[$i]);
738    $is_not = $token_modifiers[$i]&QST_NOT;
739    $counter = 0;
740
741    for ($j=0; $j<count($token_tag_scores[$i]); $j++)
742    {
743      if ($is_not)
744      {
745        if ($token_tag_scores[$i][$j] < 0.8 ||
746              ($j>0 && $token_tag_scores[$i][$j] < $token_tag_scores[$i][0]) )
747        {
748          array_splice($token_tag_scores[$i], $j);
749          array_splice($token_tag_ids[$i], $j);
750        }
751      }
752      else
753      {
754        $tag_id = $token_tag_ids[$i][$j];
755        $counter += $all_tags[$tag_id]['counter'];
756        if ($counter > 200 && $j>0 && $token_tag_scores[$i][0] > $token_tag_scores[$i][$j] )
757        {// "many" images in previous tags and starting from this tag is less relevent
758          array_splice($token_tag_ids[$i], $j);
759          array_splice($token_tag_scores[$i], $j);
760          break;
761        }
762      }
763    }
764
765    if ($is_not)
766    {
767      $not_tag_ids = array_merge($not_tag_ids, $token_tag_ids[$i]);
768    }
769  }
770
771  $all_tags = array_diff_key($all_tags, array_flip($not_tag_ids));
772  usort($all_tags, 'tag_alpha_compare');
773  foreach ( $all_tags as &$tag )
774  {
775    $tag['name'] = trigger_event('render_tag_name', $tag['name'], $tag);
776  }
777  $qsr->all_tags = $all_tags;
778
779  $qsr->tag_ids = $token_tag_ids;
780  $qsr->tag_iids = array_fill(0, count($tokens), array() );
781
782  for ($i=0; $i<count($tokens); $i++)
783  {
784    $tag_ids = $token_tag_ids[$i];
785
786    if (!empty($tag_ids))
787    {
788      $query = '
789SELECT image_id FROM '.IMAGE_TAG_TABLE.'
790  WHERE tag_id IN ('.implode(',',$tag_ids).')
791  GROUP BY image_id';
792      $qsr->tag_iids[$i] = query2array($query, null, 'image_id');
793    }
794  }
795}
796
797
798function qsearch_eval(QMultiToken $expr, QResults $qsr, &$qualifies, &$ignored_terms)
799{
800  $qualifies = false; // until we find at least one positive term
801  $ignored_terms = array();
802
803  $ids = $not_ids = array();
804
805  for ($i=0; $i<count($expr->tokens); $i++)
806  {
807    $crt = $expr->tokens[$i];
808    if ($crt->is_single)
809    {
810      $crt_ids = $qsr->iids[$crt->idx] = array_unique( array_merge($qsr->images_iids[$crt->idx], $qsr->tag_iids[$crt->idx]) );
811      $crt_qualifies = count($crt_ids)>0 || count($qsr->tag_ids[$crt->idx])>0;
812      $crt_ignored_terms = $crt_qualifies ? array() : array($crt->token);
813    }
814    else
815      $crt_ids = qsearch_eval($crt, $qsr, $crt_qualifies, $crt_ignored_terms);
816
817    $modifier = $expr->token_modifiers[$i];
818    if ($modifier & QST_NOT)
819      $not_ids = array_unique( array_merge($not_ids, $crt_ids));
820    else
821    {
822      $ignored_terms = array_merge($ignored_terms, $crt_ignored_terms);
823      if ($modifier & QST_OR)
824      {
825        $ids = array_unique( array_merge($ids, $crt_ids) );
826        $qualifies |= $crt_qualifies;
827      }
828      elseif ($crt_qualifies)
829      {
830        if ($qualifies)
831          $ids = array_intersect($ids, $crt_ids);
832        else
833          $ids = $crt_ids;
834        $qualifies = true;
835      }
836    }
837  }
838
839  if (count($not_ids))
840    $ids = array_diff($ids, $not_ids);
841  return $ids;
842}
843
844/**
845 * Returns the search results corresponding to a quick/query search.
846 * A quick/query search returns many items (search is not strict), but results
847 * are sorted by relevance unless $super_order_by is true. Returns:
848 *  array (
849 *    'items' => array of matching images
850 *    'qs'    => array(
851 *      'unmatched_terms' => array of terms from the input string that were not matched
852 *      'matching_tags' => array of matching tags
853 *      'matching_cats' => array of matching categories
854 *      'matching_cats_no_images' =>array(99) - matching categories without images
855 *      )
856 *    )
857 *
858 * @param string $q
859 * @param bool $super_order_by
860 * @param string $images_where optional additional restriction on images table
861 * @return array
862 */
863function get_quick_search_results($q, $super_order_by, $images_where='')
864{
865  global $conf;
866  //@TODO: maybe cache for 10 minutes the result set to avoid many expensive sql calls when navigating the pictures
867  $search_results =
868    array(
869      'items' => array(),
870      'qs' => array('q'=>stripslashes($q)),
871    );
872  $q = trim($q);
873  $expression = new QExpression($q);
874//var_export($expression);
875
876  $qsr = new QResults;
877  qsearch_get_tags($expression, $qsr);
878  qsearch_get_images($expression, $qsr);
879//var_export($qsr->all_tags);
880
881  $ids = qsearch_eval($expression, $qsr, $tmp, $search_results['qs']['unmatched_terms']);
882
883  $debug[] = "<!--\nparsed: ".$expression;
884  $debug[] = count($expression->stokens).' tokens';
885  for ($i=0; $i<count($expression->stokens); $i++)
886  {
887    $debug[] = $expression->stokens[$i].': '.count($qsr->tag_ids[$i]).' tags, '.count($qsr->tag_iids[$i]).' tiids, '.count($qsr->images_iids[$i]).' iiids, '.count($qsr->iids[$i]).' iids';
888  }
889  $debug[] = 'before perms '.count($ids);
890
891  $search_results['qs']['matching_tags'] = $qsr->all_tags;
892  global $template;
893
894  if (empty($ids))
895  {
896    $debug[] = '-->';
897    $template->append('footer_elements', implode("\n", $debug) );
898    return $search_results;
899  }
900
901  $where_clauses = array();
902  $where_clauses[]='i.id IN ('. implode(',', $ids) . ')';
903  if (!empty($images_where))
904  {
905    $where_clauses[]='('.$images_where.')';
906  }
907  $where_clauses[] = get_sql_condition_FandF(
908      array
909        (
910          'forbidden_categories' => 'category_id',
911          'visible_categories' => 'category_id',
912          'visible_images' => 'i.id'
913        ),
914      null,true
915    );
916
917  $query = '
918SELECT DISTINCT(id)
919  FROM '.IMAGES_TABLE.' i
920    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
921  WHERE '.implode("\n AND ", $where_clauses)."\n".
922  $conf['order_by'];
923
924  $ids = query2array($query, null, 'id');
925
926  $debug[] = count($ids).' final photo count -->';
927  $template->append('footer_elements', implode("\n", $debug) );
928
929  $search_results['items'] = $ids;
930  return $search_results;
931}
932
933/**
934 * Returns an array of 'items' corresponding to the search id.
935 * It can be either a quick search or a regular search.
936 *
937 * @param int $search_id
938 * @param bool $super_order_by
939 * @param string $images_where optional aditional restriction on images table
940 * @return array
941 */
942function get_search_results($search_id, $super_order_by, $images_where='')
943{
944  $search = get_search_array($search_id);
945  if ( !isset($search['q']) )
946  {
947    $result['items'] = get_regular_search_results($search, $images_where);
948    return $result;
949  }
950  else
951  {
952    return get_quick_search_results($search['q'], $super_order_by, $images_where);
953  }
954}
955
956?>
Note: See TracBrowser for help on using the repository browser.