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

Last change on this file since 4325 was 4325, checked in by nikrou, 14 years ago

Feature 1244 resolved
Replace all mysql functions in core code by ones independant of database engine

Fix small php code synxtax : hash must be accessed with [ ] and not { }.

  • Property svn:eol-style set to LF
File size: 16.6 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based picture gallery                                  |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2009 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/**
26 * returns search rules stored into a serialized array in "search"
27 * table. Each search rules set is numericaly identified.
28 *
29 * @param int search_id
30 * @return array
31 */
32function get_search_array($search_id)
33{
34  if (!is_numeric($search_id))
35  {
36    die('Search id must be an integer');
37  }
38
39  $query = '
40SELECT rules
41  FROM '.SEARCH_TABLE.'
42  WHERE id = '.$search_id.'
43;';
44  list($serialized_rules) = pwg_db_fetch_row(pwg_query($query));
45
46  return unserialize($serialized_rules);
47}
48
49/**
50 * returns the SQL clause from a search identifier
51 *
52 * Search rules are stored in search table as a serialized array. This array
53 * need to be transformed into an SQL clause to be used in queries.
54 *
55 * @param array search
56 * @return string
57 */
58function get_sql_search_clause($search)
59{
60  // SQL where clauses are stored in $clauses array during query
61  // construction
62  $clauses = array();
63
64  foreach (array('file','name','comment','author') as $textfield)
65  {
66    if (isset($search['fields'][$textfield]))
67    {
68      $local_clauses = array();
69      foreach ($search['fields'][$textfield]['words'] as $word)
70      {
71        array_push($local_clauses, $textfield." LIKE '%".$word."%'");
72      }
73
74      // adds brackets around where clauses
75      $local_clauses = prepend_append_array_items($local_clauses, '(', ')');
76
77      array_push(
78        $clauses,
79        implode(
80          ' '.$search['fields'][$textfield]['mode'].' ',
81          $local_clauses
82          )
83        );
84    }
85  }
86
87  if (isset($search['fields']['allwords']))
88  {
89    $fields = array('file', 'name', 'comment', 'author');
90    // in the OR mode, request bust be :
91    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
92    // OR (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
93    //
94    // in the AND mode :
95    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
96    // AND (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
97    $word_clauses = array();
98    foreach ($search['fields']['allwords']['words'] as $word)
99    {
100      $field_clauses = array();
101      foreach ($fields as $field)
102      {
103        array_push($field_clauses, $field." LIKE '%".$word."%'");
104      }
105      // adds brackets around where clauses
106      array_push(
107        $word_clauses,
108        implode(
109          "\n          OR ",
110          $field_clauses
111          )
112        );
113    }
114
115    array_walk(
116      $word_clauses,
117      create_function('&$s','$s="(".$s.")";')
118      );
119
120    array_push(
121      $clauses,
122      "\n         ".
123      implode(
124        "\n         ".
125              $search['fields']['allwords']['mode'].
126        "\n         ",
127        $word_clauses
128        )
129      );
130  }
131
132  foreach (array('date_available', 'date_creation') as $datefield)
133  {
134    if (isset($search['fields'][$datefield]))
135    {
136      array_push(
137        $clauses,
138        $datefield." = '".$search['fields'][$datefield]['date']."'"
139        );
140    }
141
142    foreach (array('after','before') as $suffix)
143    {
144      $key = $datefield.'-'.$suffix;
145
146      if (isset($search['fields'][$key]))
147      {
148        array_push(
149          $clauses,
150
151          $datefield.
152          ($suffix == 'after'             ? ' >' : ' <').
153          ($search['fields'][$key]['inc'] ? '='  : '').
154          " '".$search['fields'][$key]['date']."'"
155
156          );
157      }
158    }
159  }
160
161  if (isset($search['fields']['cat']))
162  {
163    if ($search['fields']['cat']['sub_inc'])
164    {
165      // searching all the categories id of sub-categories
166      $cat_ids = get_subcat_ids($search['fields']['cat']['words']);
167    }
168    else
169    {
170      $cat_ids = $search['fields']['cat']['words'];
171    }
172
173    $local_clause = 'category_id IN ('.implode(',', $cat_ids).')';
174    array_push($clauses, $local_clause);
175  }
176
177  // adds brackets around where clauses
178  $clauses = prepend_append_array_items($clauses, '(', ')');
179
180  $where_separator =
181    implode(
182      "\n    ".$search['mode'].' ',
183      $clauses
184      );
185
186  $search_clause = $where_separator;
187
188  return $search_clause;
189}
190
191/**
192 * returns the list of items corresponding to the advanced search array
193 *
194 * @param array search
195 * @return array
196 */
197function get_regular_search_results($search, $images_where)
198{
199  global $conf;
200  $forbidden = get_sql_condition_FandF(
201        array
202          (
203            'forbidden_categories' => 'category_id',
204            'visible_categories' => 'category_id',
205            'visible_images' => 'id'
206          ),
207        "\n  AND"
208    );
209
210  $items = array();
211  $tag_items = array();
212
213  if (isset($search['fields']['tags']))
214  {
215    $tag_items = get_image_ids_for_tags(
216      $search['fields']['tags']['words'],
217      $search['fields']['tags']['mode']
218      );
219  }
220
221  $search_clause = get_sql_search_clause($search);
222
223  if (!empty($search_clause))
224  {
225    $query = '
226SELECT DISTINCT(id)
227  FROM '.IMAGES_TABLE.' i
228    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
229  WHERE '.$search_clause;
230    if (!empty($images_where))
231    {
232      $query .= "\n  AND ".$images_where;
233    }
234    if (empty($tag_items) or $search['mode']=='AND')
235    { // directly use forbidden and order by
236      $query .= $forbidden.'
237  '.$conf['order_by'];
238    }
239    $items = array_from_query($query, 'id');
240  }
241
242  if ( !empty($tag_items) )
243  {
244    $need_permission_check = false;
245    switch ($search['mode'])
246    {
247      case 'AND':
248        if (empty($search_clause))
249        {
250          $need_permission_check = true;
251          $items = $tag_items;
252        }
253        else
254        {
255          $items = array_intersect($items, $tag_items);
256        }
257        break;
258      case 'OR':
259        $before_count = count($items);
260        $items = array_unique(
261          array_merge(
262            $items,
263            $tag_items
264            )
265          );
266        if ( $before_count < count($items) )
267        {
268          $need_permission_check = true;
269        }
270        break;
271    }
272    if ($need_permission_check and count($items) )
273    {
274      $query = '
275SELECT DISTINCT(id)
276  FROM '.IMAGES_TABLE.' i
277    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
278  WHERE id IN ('.implode(',', $items).') '.$forbidden;
279      if (!empty($images_where))
280      {
281        $query .= "\n  AND ".$images_where;
282      }
283      $query .= '
284  '.$conf['order_by'];
285      $items = array_from_query($query, 'id');
286    }
287  }
288
289  return $items;
290}
291
292/**
293 * returns the LIKE sql clause corresponding to the quick search query $q
294 * and the field $field. example q='john bill', field='file' will return
295 * file LIKE '%john%' OR file LIKE '%bill%'. Special characters for MySql full
296 * text search (+,<,>,~) are omitted. The query can contain a phrase:
297 * 'Pierre "New York"' will return LIKE '%Pierre%' OR LIKE '%New York%'.
298 * @param string q
299 * @param string field
300 * @return string
301 */
302function get_qsearch_like_clause($q, $field, $before='%', $after='%')
303{
304  $q = stripslashes($q);
305  $tokens = array();
306  $token_modifiers = array();
307  $crt_token = "";
308  $crt_token_modifier = "";
309  $state = 0;
310
311  for ($i=0; $i<strlen($q); $i++)
312  {
313    $ch = $q[$i];
314    switch ($state)
315    {
316      case 0:
317        if ($ch=='"')
318        {
319          if (strlen($crt_token))
320          {
321            $tokens[] = $crt_token;
322            $token_modifiers[] = $crt_token_modifier;
323            $crt_token = "";
324            $crt_token_modifier = "";
325          }
326          $state=1;
327        }
328        elseif ( $ch=='*' )
329        { // wild card
330          $crt_token .= '%';
331        }
332        elseif ( strcspn($ch, '+-><~')==0 )
333        { //special full text modifier
334          if (strlen($crt_token))
335          {
336            $tokens[] = $crt_token;
337            $token_modifiers[] = $crt_token_modifier;
338            $crt_token = "";
339            $crt_token_modifier = "";
340          }
341          $crt_token_modifier .= $ch;
342        }
343        elseif (preg_match('/[\s,.;!\?]+/', $ch))
344        { // white space
345          if (strlen($crt_token))
346          {
347            $tokens[] = $crt_token;
348            $token_modifiers[] = $crt_token_modifier;
349            $crt_token = "";
350            $crt_token_modifier = "";
351          }
352        }
353        else
354        {
355          if ( strcspn($ch, '%_')==0)
356          {// escape LIKE specials %_
357            $ch = '\\'.$ch;
358          }
359          $crt_token .= $ch;
360        }
361        break;
362      case 1: // qualified with quotes
363        switch ($ch)
364        {
365          case '"':
366            $tokens[] = $crt_token;
367            $token_modifiers[] = $crt_token_modifier;
368            $crt_token = "";
369            $crt_token_modifier = "";
370            $state=0;
371            break;
372          default:
373            if ( strcspn($ch, '%_')==0)
374            {// escape LIKE specials %_
375                $ch = '\\'.$ch;
376            }
377            $crt_token .= $ch;
378        }
379        break;
380    }
381  }
382  if (strlen($crt_token))
383  {
384    $tokens[] = $crt_token;
385    $token_modifiers[] = $crt_token_modifier;
386  }
387
388  $clauses = array();
389  for ($i=0; $i<count($tokens); $i++)
390  {
391    $tokens[$i] = trim($tokens[$i], '%');
392    if (strstr($token_modifiers[$i], '-')!==false)
393      continue;
394    if ( strlen($tokens[$i])==0)
395      continue;
396    $clauses[] = $field.' LIKE "'.$before.addslashes($tokens[$i]).$after.'"';
397  }
398
399  return count($clauses) ? '('.implode(' OR ', $clauses).')' : null;
400}
401
402
403/**
404 * returns the search results corresponding to a quick/query search.
405 * A quick/query search returns many items (search is not strict), but results
406 * are sorted by relevance unless $super_order_by is true. Returns:
407 * array (
408 * 'items' => array(85,68,79...)
409 * 'qs'    => array(
410 *    'matching_tags' => array of matching tags
411 *    'matching_cats' => array of matching categories
412 *    'matching_cats_no_images' =>array(99) - matching categories without images
413 *      ))
414 *
415 * @param string q
416 * @param bool super_order_by
417 * @param string images_where optional aditional restriction on images table
418 * @return array
419 */
420function get_quick_search_results($q, $super_order_by, $images_where='')
421{
422  $search_results =
423    array(
424      'items' => array(),
425      'qs' => array('q'=>stripslashes($q)),
426    );
427  $q = trim($q);
428  if (empty($q))
429  {
430    return $search_results;
431  }
432  $q_like_field = '@@__db_field__@@'; //something never in a search
433  $q_like_clause = get_qsearch_like_clause($q, $q_like_field );
434
435
436  // Step 1 - first we find matches in #images table ===========================
437  $where_clauses='MATCH(i.name, i.comment) AGAINST( "'.$q.'" IN BOOLEAN MODE)';
438  if (!empty($q_like_clause))
439  {
440    $where_clauses .= '
441    OR '. str_replace($q_like_field, 'CONVERT(file, CHAR)', $q_like_clause);
442    $where_clauses = '('.$where_clauses.')';
443  }
444  $where_clauses = array($where_clauses);
445  if (!empty($images_where))
446  {
447    $where_clauses[]='('.$images_where.')';
448  }
449  $where_clauses[] .= get_sql_condition_FandF
450      (
451        array( 'visible_images' => 'i.id' ), null, true
452      );
453  $query = '
454SELECT i.id,
455    MATCH(i.name, i.comment) AGAINST( "'.$q.'" IN BOOLEAN MODE) AS weight
456  FROM '.IMAGES_TABLE.' i
457  WHERE '.implode("\n AND ", $where_clauses);
458
459  $by_weights=array();
460  $result = pwg_query($query);
461  while ($row = pwg_db_fetch_assoc($result))
462  { // weight is important when sorting images by relevance
463    if ($row['weight'])
464    {
465      $by_weights[(int)$row['id']] =  2*$row['weight'];
466    }
467    else
468    {//full text does not match but file name match
469      $by_weights[(int)$row['id']] =  2;
470    }
471  }
472
473
474  // Step 2 - search tags corresponding to the query $q ========================
475  if (!empty($q_like_clause))
476  { // search name and url name (without accents)
477    $query = '
478SELECT id, name, url_name
479  FROM '.TAGS_TABLE.'
480  WHERE ('.str_replace($q_like_field, 'CONVERT(name, CHAR)', $q_like_clause).'
481    OR '.str_replace($q_like_field, 'url_name', $q_like_clause).')';
482    $tags = hash_from_query($query, 'id');
483    if ( !empty($tags) )
484    { // we got some tags; get the images
485      $search_results['qs']['matching_tags']=$tags;
486      $query = '
487SELECT image_id, COUNT(tag_id) AS weight
488  FROM '.IMAGE_TAG_TABLE.'
489  WHERE tag_id IN ('.implode(',',array_keys($tags)).')
490  GROUP BY image_id';
491      $result = pwg_query($query);
492      while ($row = pwg_db_fetch_assoc($result))
493      { // weight is important when sorting images by relevance
494        $image_id=(int)$row['image_id'];
495        @$by_weights[$image_id] += $row['weight'];
496      }
497    }
498  }
499
500
501  // Step 3 - search categories corresponding to the query $q ==================
502  global $user;
503  $query = '
504SELECT id, name, permalink, nb_images
505  FROM '.CATEGORIES_TABLE.'
506    INNER JOIN '.USER_CACHE_CATEGORIES_TABLE.' ON id=cat_id
507  WHERE user_id='.$user['id'].'
508    AND MATCH(name, comment) AGAINST( "'.$q.'" IN BOOLEAN MODE)'.
509  get_sql_condition_FandF (
510      array( 'visible_categories' => 'cat_id' ), "\n    AND"
511    );
512  $result = pwg_query($query);
513  while ($row = pwg_db_fetch_assoc($result))
514  { // weight is important when sorting images by relevance
515    if ($row['nb_images']==0)
516    {
517      $search_results['qs']['matching_cats_no_images'][] = $row;
518    }
519    else
520    {
521      $search_results['qs']['matching_cats'][$row['id']] = $row;
522    }
523  }
524
525  if ( empty($by_weights) and empty($search_results['qs']['matching_cats']) )
526  {
527    return $search_results;
528  }
529
530  // Step 4 - now we have $by_weights ( array image id => weight ) that need
531  // permission checks and/or matching categories to get images from
532  $where_clauses = array();
533  if ( !empty($by_weights) )
534  {
535    $where_clauses[]='i.id IN ('
536      . implode(',', array_keys($by_weights)) . ')';
537  }
538  if ( !empty($search_results['qs']['matching_cats']) )
539  {
540    $where_clauses[]='category_id IN ('.
541      implode(',',array_keys($search_results['qs']['matching_cats'])).')';
542  }
543  $where_clauses = array( '('.implode("\n    OR ",$where_clauses).')' );
544  if (!empty($images_where))
545  {
546    $where_clauses[]='('.$images_where.')';
547  }
548  $where_clauses[] = get_sql_condition_FandF(
549      array
550        (
551          'forbidden_categories' => 'category_id',
552          'visible_categories' => 'category_id',
553          'visible_images' => 'i.id'
554        ),
555      null,true
556    );
557
558  global $conf;
559  $query = '
560SELECT DISTINCT(id)
561  FROM '.IMAGES_TABLE.' i
562    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
563  WHERE '.implode("\n AND ", $where_clauses)."\n".
564  $conf['order_by'];
565
566  $allowed_images = array_from_query( $query, 'id');
567
568  if ( $super_order_by or empty($by_weights) )
569  {
570    $search_results['items'] = $allowed_images;
571    return $search_results;
572  }
573
574  $allowed_images = array_flip( $allowed_images );
575  $divisor = 5.0 * count($allowed_images);
576  foreach ($allowed_images as $id=>$rank )
577  {
578    $weight = isset($by_weights[$id]) ? $by_weights[$id] : 1;
579    $weight -= $rank/$divisor;
580    $allowed_images[$id] = $weight;
581  }
582  arsort($allowed_images, SORT_NUMERIC);
583  $search_results['items'] = array_keys($allowed_images);
584  return $search_results;
585}
586
587/**
588 * returns an array of 'items' corresponding to the search id
589 *
590 * @param int search id
591 * @param string images_where optional aditional restriction on images table
592 * @return array
593 */
594function get_search_results($search_id, $super_order_by, $images_where='')
595{
596  $search = get_search_array($search_id);
597  if ( !isset($search['q']) )
598  {
599    $result['items'] = get_regular_search_results($search, $images_where);
600    return $result;
601  }
602  else
603  {
604    return get_quick_search_results($search['q'], $super_order_by, $images_where);
605  }
606}
607?>
Note: See TracBrowser for help on using the repository browser.