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

Last change on this file since 2299 was 2299, checked in by plg, 16 years ago

Bug fixed: as rvelices notified me by email, my header replacement script was
bugged (r2297 was repeating new and old header).

By the way, I've also removed the replacement keywords. We were using them
because it was a common usage with CVS but it is advised not to use them with
Subversion. Personnaly, it is a problem when I search differences between 2
Piwigo installations outside Subversion.

  • Property svn:eol-style set to LF
  • Property svn:keywords set to Author Date Id Revision
File size: 15.2 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based picture gallery                                  |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008      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) = mysql_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)
198{
199  $items = array();
200
201  $search_clause = get_sql_search_clause($search);
202
203  if (!empty($search_clause))
204  {
205    $query = '
206SELECT DISTINCT(id)
207  FROM '.IMAGES_TABLE.'
208    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
209  WHERE '.$search_clause.'
210;';
211    $items = array_from_query($query, 'id');
212  }
213
214  if (isset($search['fields']['tags']))
215  {
216    $tag_items = get_image_ids_for_tags(
217      $search['fields']['tags']['words'],
218      $search['fields']['tags']['mode']
219      );
220
221    switch ($search['mode'])
222    {
223      case 'AND':
224      {
225        if (empty($search_clause))
226        {
227          $items = $tag_items;
228        }
229        else
230        {
231          $items = array_intersect($items, $tag_items);
232        }
233        break;
234      }
235      case 'OR':
236      {
237        $items = array_unique(
238          array_merge(
239            $items,
240            $tag_items
241            )
242          );
243        break;
244      }
245    }
246  }
247
248  return $items;
249}
250
251/**
252 * returns the LIKE sql clause corresponding to the quick search query $q
253 * and the field $field. example q='john bill', field='file' will return
254 * file LIKE '%john%' OR file LIKE '%bill%'. Special characters for MySql full
255 * text search (+,<,>,~) are omitted. The query can contain a phrase:
256 * 'Pierre "New York"' will return LIKE '%Pierre%' OR LIKE '%New York%'.
257 * @param string q
258 * @param string field
259 * @return string
260 */
261function get_qsearch_like_clause($q, $field)
262{
263  $q = stripslashes($q);
264  $tokens = array();
265  $token_modifiers = array();
266  $crt_token = "";
267  $crt_token_modifier = "";
268  $state = 0;
269
270  for ($i=0; $i<strlen($q); $i++)
271  {
272    $ch = $q[$i];
273    switch ($state)
274    {
275      case 0:
276        if ($ch=='"')
277        {
278          if (strlen($crt_token))
279          {
280            $tokens[] = $crt_token;
281            $token_modifiers[] = $crt_token_modifier;
282            $crt_token = "";
283            $crt_token_modifier = "";
284          }
285          $state=1;
286        }
287        elseif ( $ch=='*' )
288        { // wild card
289          $crt_token .= '%';
290        }
291        elseif ( strcspn($ch, '+-><~')==0 )
292        { //special full text modifier
293          if (strlen($crt_token))
294          {
295            $tokens[] = $crt_token;
296            $token_modifiers[] = $crt_token_modifier;
297            $crt_token = "";
298            $crt_token_modifier = "";
299          }
300          $crt_token_modifier .= $ch;
301        }
302        elseif (preg_match('/[\s,.;!\?]+/', $ch))
303        { // white space
304          if (strlen($crt_token))
305          {
306            $tokens[] = $crt_token;
307            $token_modifiers[] = $crt_token_modifier;
308            $crt_token = "";
309            $crt_token_modifier = "";
310          }
311        }
312        else
313        {
314          $crt_token .= $ch;
315        }
316        break;
317      case 1: // qualified with quotes
318        switch ($ch)
319        {
320          case '"':
321            $tokens[] = $crt_token;
322            $token_modifiers[] = $crt_token_modifier;
323            $crt_token = "";
324            $crt_token_modifier = "";
325            $state=0;
326            break;
327          default:
328            $crt_token .= $ch;
329        }
330        break;
331    }
332  }
333  if (strlen($crt_token))
334  {
335    $tokens[] = $crt_token;
336    $token_modifiers[] = $crt_token_modifier;
337  }
338
339  $clauses = array();
340  for ($i=0; $i<count($tokens); $i++)
341  {
342    $tokens[$i] = trim($tokens[$i], '%');
343    if (strstr($token_modifiers[$i], '-')!==false)
344      continue;
345    if ( strlen($tokens[$i])==0)
346      continue;
347    $clauses[] = $field.' LIKE "%'.addslashes($tokens[$i]).'%"';
348  }
349
350  return count($clauses) ? '('.implode(' OR ', $clauses).')' : null;
351}
352
353
354/**
355 * returns the search results corresponding to a quick/query search.
356 * A quick/query search returns many items (search is not strict), but results
357 * are sorted by relevance unless $page['super_order_by'] is set. Returns:
358 * array (
359 * 'items' => array(85,68,79...)
360 * 'as_is' => 1 (indicates the caller that items are ordered and permissions checked
361 * 'qs'    => array(
362 *    'matching_tags' => array of matching tags
363 *    'matching_cats' => array of matching categories
364 *    'matching_cats_no_images' =>array(99) - matching categories without images
365 *      ))
366 *
367 * @param string q
368 * @param string images_where optional aditional restriction on images table
369 * @return array
370 */
371function get_quick_search_results($q, $images_where='')
372{
373  global $page;
374  $search_results =
375    array(
376      'items' => array(),
377      'as_is' => 1,
378      'qs' => array('q'=>stripslashes($q)),
379    );
380  $q = trim($q);
381  if (empty($q))
382  {
383    return $search_results;
384  }
385  $q_like_field = '@@__db_field__@@'; //something never in a search
386  $q_like_clause = get_qsearch_like_clause($q, $q_like_field );
387
388
389  // Step 1 - first we find matches in #images table ===========================
390  $where_clauses='MATCH(i.name, i.comment) AGAINST( "'.$q.'" IN BOOLEAN MODE)';
391  if (!empty($q_like_clause))
392  {
393    $where_clauses .= '
394    OR '. str_replace($q_like_field, 'file', $q_like_clause);
395    $where_clauses = '('.$where_clauses.')';
396  }
397  $where_clauses = array($where_clauses);
398  if (!empty($images_where))
399  {
400    $where_clauses[]='('.$images_where.')';
401  }
402  $where_clauses[] .= get_sql_condition_FandF
403      (
404        array( 'visible_images' => 'i.id' ), null, true
405      );
406  $query = '
407SELECT i.id,
408    MATCH(i.name, i.comment) AGAINST( "'.$q.'" IN BOOLEAN MODE) AS weight
409  FROM '.IMAGES_TABLE.' i
410  WHERE '.implode("\n AND ", $where_clauses);
411
412  $by_weights=array();
413  $result = pwg_query($query);
414  while ($row = mysql_fetch_array($result))
415  { // weight is important when sorting images by relevance
416    if ($row['weight'])
417    {
418      $by_weights[(int)$row['id']] =  2*$row['weight'];
419    }
420    else
421    {//full text does not match but file name match
422      $by_weights[(int)$row['id']] =  2;
423    }
424  }
425
426
427  // Step 2 - search tags corresponding to the query $q ========================
428  if (!empty($q_like_clause))
429  { // search name and url name (without accents)
430    $query = '
431SELECT id, name, url_name
432  FROM '.TAGS_TABLE.'
433  WHERE ('.str_replace($q_like_field, 'CONVERT(name, CHAR)', $q_like_clause).'
434    OR '.str_replace($q_like_field, 'url_name', $q_like_clause).')';
435    $tags = hash_from_query($query, 'id');
436    if ( !empty($tags) )
437    { // we got some tags; get the images
438      $search_results['qs']['matching_tags']=$tags;
439      $query = '
440SELECT image_id, COUNT(tag_id) AS weight
441  FROM '.IMAGE_TAG_TABLE.'
442  WHERE tag_id IN ('.implode(',',array_keys($tags)).')
443  GROUP BY image_id';
444      $result = pwg_query($query);
445      while ($row = mysql_fetch_assoc($result))
446      { // weight is important when sorting images by relevance
447        $image_id=(int)$row['image_id'];
448        @$by_weights[$image_id] += $row['weight'];
449      }
450    }
451  }
452
453
454  // Step 3 - search categories corresponding to the query $q ==================
455  global $user;
456  $query = '
457SELECT id, name, permalink, nb_images
458  FROM '.CATEGORIES_TABLE.'
459    INNER JOIN '.USER_CACHE_CATEGORIES_TABLE.' ON id=cat_id
460  WHERE user_id='.$user['id'].'
461    AND MATCH(name, comment) AGAINST( "'.$q.'" IN BOOLEAN MODE)'.
462  get_sql_condition_FandF (
463      array( 'visible_categories' => 'cat_id' ), "\n    AND"
464    );
465  $result = pwg_query($query);
466  while ($row = mysql_fetch_assoc($result))
467  { // weight is important when sorting images by relevance
468    if ($row['nb_images']==0)
469    {
470      $search_results['qs']['matching_cats_no_images'][] = $row;
471    }
472    else
473    {
474      $search_results['qs']['matching_cats'][$row['id']] = $row;
475    }
476  }
477
478  if ( empty($by_weights) and empty($search_results['qs']['matching_cats']) )
479  {
480    return $search_results;
481  }
482
483  // Step 4 - now we have $by_weights ( array image id => weight ) that need
484  // permission checks and/or matching categories to get images from
485  $where_clauses = array();
486  if ( !empty($by_weights) )
487  {
488    $where_clauses[]='i.id IN ('
489      . implode(',', array_keys($by_weights)) . ')';
490  }
491  if ( !empty($search_results['qs']['matching_cats']) )
492  {
493    $where_clauses[]='category_id IN ('.
494      implode(',',array_keys($search_results['qs']['matching_cats'])).')';
495  }
496  $where_clauses = array( '('.implode("\n    OR ",$where_clauses).')' );
497  if (!empty($images_where))
498  {
499    $where_clauses[]='('.$images_where.')';
500  }
501  $where_clauses[] = get_sql_condition_FandF(
502      array
503        (
504          'forbidden_categories' => 'category_id',
505          'visible_categories' => 'category_id',
506          'visible_images' => 'i.id'
507        ),
508      null,true
509    );
510
511  global $conf;
512  $query = '
513SELECT DISTINCT(id)
514  FROM '.IMAGES_TABLE.' i
515    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
516  WHERE '.implode("\n AND ", $where_clauses)."\n".
517  $conf['order_by'];
518
519  $allowed_images = array_from_query( $query, 'id');
520
521  if ( isset($page['super_order_by']) or empty($by_weights) )
522  {
523    $search_results['items'] = $allowed_images;
524    return $search_results;
525  }
526
527  $allowed_images = array_flip( $allowed_images );
528  $divisor = 5.0 * count($allowed_images);
529  foreach ($allowed_images as $id=>$rank )
530  {
531    $weight = isset($by_weights[$id]) ? $by_weights[$id] : 1;
532    $weight -= $rank/$divisor;
533    $allowed_images[$id] = $weight;
534  }
535  arsort($allowed_images, SORT_NUMERIC);
536  $search_results['items'] = array_keys($allowed_images);
537  return $search_results;
538}
539
540/**
541 * returns an array of 'items' corresponding to the search id
542 *
543 * @param int search id
544 * @param string images_where optional aditional restriction on images table
545 * @return array
546 */
547function get_search_results($search_id, $images_where='')
548{
549  $search = get_search_array($search_id);
550  if ( !isset($search['q']) )
551  {
552    $result['items'] = get_regular_search_results($search);
553    return $result;
554  }
555  else
556  {
557    return get_quick_search_results($search['q'], $images_where);
558  }
559}
560?>
Note: See TracBrowser for help on using the repository browser.